From c7b39b14f15d5b740960e7d40876b0852f7adb9a Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 07:58:29 +0200
Subject: [PATCH 01/20] =?UTF-8?q?=20N=C2=B08796=20-=20Add=20PHP=20code=20s?=
=?UTF-8?q?tyle=20validation=20in=20iTop=20and=20extensions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 4 ++++
Jenkinsfile | 11 ++++++++++-
test/php-code-style/.php-cs-fixer.dist.php | 19 +++++++++++++++++++
test/php-code-style/README.md | 20 ++++++++++++++++++++
test/php-code-style/composer.json | 7 +++++++
5 files changed, 60 insertions(+), 1 deletion(-)
create mode 100644 test/php-code-style/.php-cs-fixer.dist.php
create mode 100644 test/php-code-style/README.md
create mode 100644 test/php-code-style/composer.json
diff --git a/.gitignore b/.gitignore
index 5166a03..5880cf8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,7 @@ data/*.txt
.buildpath
.project
.settings/*
+test/php-code-style/vendor
+.php-cs-fixer.cache
+.phpunit.result.cache
+phpstan_results.xml
\ No newline at end of file
diff --git a/Jenkinsfile b/Jenkinsfile
index ffcdf13..494a3d8 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -19,7 +19,7 @@ pipeline {
stage('phpunit tests') {
steps {
script {
- sh 'mkdir logs'
+ sh 'mkdir -p logs'
sh 'php vendor/bin/phpunit --log-junit logs/phpunit_results.xml --configuration test/phpunit.xml --teamcity '
}
}
@@ -33,6 +33,15 @@ pipeline {
}
}
}
+
+ stage('code style tests') {
+ steps {
+ script {
+ sh 'cd test/php-code-style/; composer install'
+ sh 'test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php -vvv --format=junit > logs/codestyle_results.xml'
+ }
+ }
+ }
}
post {
diff --git a/test/php-code-style/.php-cs-fixer.dist.php b/test/php-code-style/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..a3cf523
--- /dev/null
+++ b/test/php-code-style/.php-cs-fixer.dist.php
@@ -0,0 +1,19 @@
+exclude('vendor')
+ ->in($APPROOT)
+ ;
+
+$config = new PhpCsFixer\Config();
+return $config->setRiskyAllowed(true)
+ ->setRules([
+ '@PSR12' => true,
+ 'no_extra_blank_lines' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ ])
+ ->setFinder($finder)
+;
\ No newline at end of file
diff --git a/test/php-code-style/README.md b/test/php-code-style/README.md
new file mode 100644
index 0000000..b33fadb
--- /dev/null
+++ b/test/php-code-style/README.md
@@ -0,0 +1,20 @@
+Code formatting tool used by iTop is PHP-CS-Fixer:
+https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/master
+
+to install it locally, run once:
+```
+cd test/php-code-style/; composer install; cd -
+```
+
+to check code style issues (no path provided means whole iTop code base):
+
+```
+test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php
+```
+
+to respect iTop code standards and re-format (no path provided means whole iTop code base):
+
+```
+test/php-code-style/vendor/bin/php-cs-fixer fix --config test/php-code-style/.php-cs-fixer.dist.php
+
+```
\ No newline at end of file
diff --git a/test/php-code-style/composer.json b/test/php-code-style/composer.json
new file mode 100644
index 0000000..1d7e9ec
--- /dev/null
+++ b/test/php-code-style/composer.json
@@ -0,0 +1,7 @@
+{
+ "require-dev": {
+ "php": "^7.4 || ^8.0",
+ "friendsofphp/php-cs-fixer": "^3.88",
+ "phpstan/phpstan": "^2.1"
+ }
+}
From bdff24f7c58a6f22de4a5c09f801f8918556b968 Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 08:02:17 +0200
Subject: [PATCH 02/20] fix phpstan not initialized
---
Jenkinsfile | 3 +++
1 file changed, 3 insertions(+)
diff --git a/Jenkinsfile b/Jenkinsfile
index 494a3d8..a587a3a 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -29,6 +29,9 @@ pipeline {
steps {
script {
sh 'mkdir -p logs'
+ if ("${phpstan_level}" == ""){
+ def phpstan_level = "1";
+ }
sh 'vendor/bin/phpstan analyse -l ${phpstan_level} --error-format=junit > logs/phpstan_results.xml'
}
}
From c7fd7476762ba6e718ad1f840ef71c17a6a6ebfc Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 08:04:23 +0200
Subject: [PATCH 03/20] check style without verbose in CI
---
Jenkinsfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Jenkinsfile b/Jenkinsfile
index a587a3a..d6ff518 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -41,7 +41,7 @@ pipeline {
steps {
script {
sh 'cd test/php-code-style/; composer install'
- sh 'test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php -vvv --format=junit > logs/codestyle_results.xml'
+ sh 'test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php --format=junit > logs/codestyle_results.xml'
}
}
}
From b89d1f49ec06612313ddbe380ff0789ed8a7781b Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 08:07:33 +0200
Subject: [PATCH 04/20] fix php-cs-fixer artifact content
---
Jenkinsfile | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/Jenkinsfile b/Jenkinsfile
index d6ff518..4181d94 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -16,11 +16,11 @@ pipeline {
}
}
- stage('phpunit tests') {
+ stage('code style tests') {
steps {
script {
- sh 'mkdir -p logs'
- sh 'php vendor/bin/phpunit --log-junit logs/phpunit_results.xml --configuration test/phpunit.xml --teamcity '
+ sh 'cd test/php-code-style/; composer install'
+ sh 'test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php --format=junit 2>&1 > logs/codestyle_results.xml'
}
}
}
@@ -36,14 +36,14 @@ pipeline {
}
}
}
+ }
- stage('code style tests') {
- steps {
- script {
- sh 'cd test/php-code-style/; composer install'
- sh 'test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php --format=junit > logs/codestyle_results.xml'
- }
- }
+ stage('phpunit tests') {
+ steps {
+ script {
+ sh 'mkdir -p logs'
+ sh 'php vendor/bin/phpunit --log-junit logs/phpunit_results.xml --configuration test/phpunit.xml --teamcity '
+ }
}
}
From d60b4e0f45dd230b64cacdce41c41354837843b8 Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 08:09:10 +0200
Subject: [PATCH 05/20] fix php-cs-fixer artifact content
---
Jenkinsfile | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/Jenkinsfile b/Jenkinsfile
index 4181d94..b465927 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -36,14 +36,14 @@ pipeline {
}
}
}
- }
- stage('phpunit tests') {
- steps {
- script {
- sh 'mkdir -p logs'
- sh 'php vendor/bin/phpunit --log-junit logs/phpunit_results.xml --configuration test/phpunit.xml --teamcity '
- }
+ stage('phpunit tests') {
+ steps {
+ script {
+ sh 'mkdir -p logs'
+ sh 'php vendor/bin/phpunit --log-junit logs/phpunit_results.xml --configuration test/phpunit.xml --teamcity '
+ }
+ }
}
}
From 356f0942250d2ccb5e35e6f48b926f492cb1006b Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 08:10:31 +0200
Subject: [PATCH 06/20] fix php-cs-fixer artifact content
---
Jenkinsfile | 1 +
1 file changed, 1 insertion(+)
diff --git a/Jenkinsfile b/Jenkinsfile
index b465927..b663095 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -20,6 +20,7 @@ pipeline {
steps {
script {
sh 'cd test/php-code-style/; composer install'
+ sh 'mkdir -p logs'
sh 'test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php --format=junit 2>&1 > logs/codestyle_results.xml'
}
}
From 659e6c0058f0417c6c311abf959d5c5104089a14 Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 08:13:34 +0200
Subject: [PATCH 07/20] keep processing other steps after code style issues
---
Jenkinsfile | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Jenkinsfile b/Jenkinsfile
index b663095..610e1e4 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -19,9 +19,11 @@ pipeline {
stage('code style tests') {
steps {
script {
+ catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
sh 'cd test/php-code-style/; composer install'
sh 'mkdir -p logs'
sh 'test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php --format=junit 2>&1 > logs/codestyle_results.xml'
+ }
}
}
}
From 50bc1589d87f46e08c8d4059dd07034b9cf44691 Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 08:18:16 +0200
Subject: [PATCH 08/20] exclude test/php-code-style from phpstan review
---
phpstan.neon.dist | 3 +++
1 file changed, 3 insertions(+)
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 5e7a590..3aab162 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -8,6 +8,9 @@ parameters:
scanDirectories:
- vendor
+ excludePaths:
+ - test/php-code-style
+
paths:
- core
- test
From 01472f96419aa0483ee46bc82b9fd62fc832cf56 Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 08:18:56 +0200
Subject: [PATCH 09/20] keep processing other steps after phpstan issues
---
Jenkinsfile | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Jenkinsfile b/Jenkinsfile
index 610e1e4..36260da 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -31,11 +31,13 @@ pipeline {
stage('phpstan tests') {
steps {
script {
+ catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
sh 'mkdir -p logs'
if ("${phpstan_level}" == ""){
def phpstan_level = "1";
}
sh 'vendor/bin/phpstan analyse -l ${phpstan_level} --error-format=junit > logs/phpstan_results.xml'
+ }
}
}
}
From a932e8310ae7a7aabebfc78773e9239a482e9663 Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 08:32:10 +0200
Subject: [PATCH 10/20] fix code style report output
---
Jenkinsfile | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Jenkinsfile b/Jenkinsfile
index 36260da..20ada1b 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -22,7 +22,8 @@ pipeline {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
sh 'cd test/php-code-style/; composer install'
sh 'mkdir -p logs'
- sh 'test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php --format=junit 2>&1 > logs/codestyle_results.xml'
+ sh 'test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php --format=junit >logs/codestyle_results.xml'
+ sh "sed -i -e 's|.*
Date: Fri, 17 Oct 2025 08:42:49 +0200
Subject: [PATCH 11/20] encapsulate whole code formatting in a bash script
---
Jenkinsfile | 4 +---
test/php-code-style/validate.sh | 7 +++++++
2 files changed, 8 insertions(+), 3 deletions(-)
create mode 100644 test/php-code-style/validate.sh
diff --git a/Jenkinsfile b/Jenkinsfile
index 20ada1b..4172618 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -20,10 +20,8 @@ pipeline {
steps {
script {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
- sh 'cd test/php-code-style/; composer install'
sh 'mkdir -p logs'
- sh 'test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php --format=junit >logs/codestyle_results.xml'
- sh "sed -i -e 's|.*logs/codestyle_results.xml
+sed -i -e 's|.*
Date: Fri, 17 Oct 2025 08:47:21 +0200
Subject: [PATCH 12/20] fix codestyle CLI
---
Jenkinsfile | 3 +--
test/php-code-style/validate.sh | 16 ++++++++++++----
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/Jenkinsfile b/Jenkinsfile
index 4172618..03aaeec 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -20,8 +20,7 @@ pipeline {
steps {
script {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
- sh 'mkdir -p logs'
- sh 'test/php-code-style/validate.sh'
+ sh 'bash test/php-code-style/validate.sh'
}
}
}
diff --git a/test/php-code-style/validate.sh b/test/php-code-style/validate.sh
index 2a0b683..8f57851 100644
--- a/test/php-code-style/validate.sh
+++ b/test/php-code-style/validate.sh
@@ -1,7 +1,15 @@
#! /bin/bash
-DIR=$(dirname $0)
-cd $DIR && composer install
+LOG_DIR=$(pwd)/logs
+if [ ! -d $LOG_DIR ]
+then
+ mkdir $LOG_DIR
+fi
+
+CODESTYLE_DIR=$(dirname $0)
+cd $CODESTYLE_DIR && composer install
cd -
-test/php-code-style/vendor/bin/php-cs-fixer check --config test/php-code-style/.php-cs-fixer.dist.php --format=junit >logs/codestyle_results.xml
-sed -i -e 's|.*$LOG_DIR/codestyle_results.xml
+sed -i -e 's|.*
Date: Fri, 17 Oct 2025 08:48:54 +0200
Subject: [PATCH 13/20] codestyle report in verbose
---
test/php-code-style/validate.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/php-code-style/validate.sh b/test/php-code-style/validate.sh
index 8f57851..8fbc4bb 100644
--- a/test/php-code-style/validate.sh
+++ b/test/php-code-style/validate.sh
@@ -10,6 +10,6 @@ CODESTYLE_DIR=$(dirname $0)
cd $CODESTYLE_DIR && composer install
cd -
-$CODESTYLE_DIR/vendor/bin/php-cs-fixer check --config $CODESTYLE_DIR/.php-cs-fixer.dist.php --format=junit >$LOG_DIR/codestyle_results.xml
+$CODESTYLE_DIR/vendor/bin/php-cs-fixer check --config $CODESTYLE_DIR/.php-cs-fixer.dist.php -vvv --format=junit >$LOG_DIR/codestyle_results.xml
sed -i -e 's|.*
Date: Fri, 17 Oct 2025 08:50:36 +0200
Subject: [PATCH 14/20] apply code style to whole repository
---
core/callitopservice.class.inc.php | 26 +-
core/collectionplan.class.inc.php | 261 +-
core/collector.class.inc.php | 2206 +++++++++--------
core/csvcollector.class.inc.php | 589 ++---
core/dopostrequestservice.class.inc.php | 7 +-
core/ioexception.class.inc.php | 2 +-
core/jsoncollector.class.inc.php | 641 +++--
core/lookuptable.class.inc.php | 339 +--
core/mappingtable.class.inc.php | 112 +-
core/orchestrator.class.inc.php | 529 ++--
core/parameters.class.inc.php | 423 ++--
core/polyfill.inc.php | 20 +-
core/restclient.class.inc.php | 379 +--
core/sqlcollector.class.inc.php | 398 ++-
core/utils.class.inc.php | 1445 +++++------
exec.php | 167 +-
test/CallItopServiceTest.php | 141 +-
test/CollectionPlanTest.php | 260 +-
test/CollectorSynchroTest.php | 297 +--
test/CollectorTest.php | 390 +--
test/CsvCollectorTest.php | 356 +--
test/FakeCollector.php | 14 +-
test/JsonCollectorTest.php | 603 ++---
test/LookupTest.php | 426 ++--
test/OrchestratorTest.php | 141 +-
test/ParametersTest.php | 82 +-
test/RestTest.php | 184 +-
test/UtilsTest.php | 463 ++--
test/bootstrap.inc.php | 2 -
.../LegacyCollector.class.inc.php | 16 +-
.../src/ExtendedCollector.class.inc.php | 3 +-
.../src/StandardCollector.class.inc.php | 1 -
.../src/TestCollectionPlan.class.inc.php | 1 -
.../iTopPersonCollector.class.inc.php | 58 +-
test/collector/attribute_isnullified/main.php | 1 +
test/getproject/common/main.php | 3 +-
test/getproject/module/module.myproject.php | 59 +-
test/single_csv/common/main.php | 3 +-
.../ITopPersonJsonCollector.class.inc.php | 43 +-
test/single_json/common/main.php | 5 +-
.../ITopPersonJsonCollector.class.inc.php | 2 +-
.../example_of_param_distrib/rest/main.php | 5 +-
toolkit/dump_tasks.php | 134 +-
toolkit/testconnection.php | 17 +-
44 files changed, 5660 insertions(+), 5594 deletions(-)
diff --git a/core/callitopservice.class.inc.php b/core/callitopservice.class.inc.php
index 919b0ba..af8500a 100644
--- a/core/callitopservice.class.inc.php
+++ b/core/callitopservice.class.inc.php
@@ -5,21 +5,21 @@
*/
class CallItopService
{
- public function CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut = -1)
- {
+ public function CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut = -1)
+ {
- $sUrl = Utils::GetConfigurationValue('itop_url', '').$sUri;
+ $sUrl = Utils::GetConfigurationValue('itop_url', '').$sUri;
- $aData = array_merge(
- Utils::GetCredentials(),
- $aAdditionalData
- );
+ $aData = array_merge(
+ Utils::GetCredentials(),
+ $aAdditionalData
+ );
- // timeout in seconds, for a synchro to run
- $iCurrentTimeOut = ($iTimeOut === -1) ? (int)Utils::GetConfigurationValue('itop_synchro_timeout', 600) : $iTimeOut;
- $aCurlOptions = Utils::GetCurlOptions($iCurrentTimeOut);
+ // timeout in seconds, for a synchro to run
+ $iCurrentTimeOut = ($iTimeOut === -1) ? (int)Utils::GetConfigurationValue('itop_synchro_timeout', 600) : $iTimeOut;
+ $aCurlOptions = Utils::GetCurlOptions($iCurrentTimeOut);
- $aResponseHeaders = null;
- return Utils::DoPostRequest($sUrl, $aData, '', $aResponseHeaders, $aCurlOptions);
- }
+ $aResponseHeaders = null;
+ return Utils::DoPostRequest($sUrl, $aData, '', $aResponseHeaders, $aCurlOptions);
+ }
}
diff --git a/core/collectionplan.class.inc.php b/core/collectionplan.class.inc.php
index 2b92236..0249d1f 100644
--- a/core/collectionplan.class.inc.php
+++ b/core/collectionplan.class.inc.php
@@ -1,4 +1,5 @@
Rank is missing from the launch_sequence of ".$aCollector['name']." It will not be launched.");
- }
- }
- array_multisort($aRank, SORT_ASC, $aSortedCollectorsLaunchSequence);
-
- return $aSortedCollectorsLaunchSequence;
- }
-
- return $aCollectorsLaunchSequence;
- }
-
- /**
- * Look for the collector definition file in the different possible collector directories
- *
- * @param $sCollector
- *
- * @return bool
- */
- public function GetCollectorDefinitionFile($sCollector): bool
- {
- if (file_exists(APPROOT.'collectors/extensions/src/'.$sCollector.'.class.inc.php')) {
- require_once(APPROOT.'collectors/extensions/src/'.$sCollector.'.class.inc.php');
- } elseif (file_exists(APPROOT.'collectors/src/'.$sCollector.'.class.inc.php')) {
- require_once(APPROOT.'collectors/src/'.$sCollector.'.class.inc.php');
- } elseif (file_exists(APPROOT.'collectors/'.$sCollector.'.class.inc.php')) {
- require_once(APPROOT.'collectors/'.$sCollector.'.class.inc.php');
- } else {
- return false;
- }
-
- return true;
- }
-
- /**
- * Add the collectors to be launched to the orchestrator
- *
- * @return bool
- * @throws \Exception
- */
- public function AddCollectorsToOrchestrator(): bool
- {
- // Read and order launch sequence
- $aCollectorsLaunchSequence = $this->GetSortedLaunchSequence();
- if (empty($aCollectorsLaunchSequence)) {
- Utils::Log(LOG_INFO, "---------- No Launch sequence has been found, no collector has been orchestrated ----------");
-
- return false;
- }
-
- $iIndex = 1;
- $aOrchestratedCollectors = [];
- foreach ($aCollectorsLaunchSequence as $iKey => $aCollector) {
- $sCollectorName = $aCollector['name'];
-
- // Skip disabled collectors
- if (!array_key_exists('enable', $aCollector) || ($aCollector['enable'] != 'yes')) {
- Utils::Log(LOG_INFO, "> ".$sCollectorName." is disabled and will not be launched.");
- continue;
- }
-
- // Read collector php definition file
- if (!$this->GetCollectorDefinitionFile($sCollectorName)) {
- Utils::Log(LOG_INFO, "> No file definition file has been found for ".$sCollectorName." It will not be launched.");
- continue;
- }
-
- /** @var Collector $oCollector */
- // Instantiate collector
- $oCollector = new $sCollectorName;
- $oCollector->Init();
- if ($oCollector->CheckToLaunch($aOrchestratedCollectors)) {
- Utils::Log(LOG_INFO, $sCollectorName.' will be launched !');
- Orchestrator::AddCollector($iIndex++, $sCollectorName);
- $aOrchestratedCollectors[$sCollectorName] = true;
- } else {
- $aOrchestratedCollectors[$sCollectorName] = false;
- }
- unset($oCollector);
- }
- Utils::Log(LOG_INFO, "---------- Collectors have been orchestrated ----------");
-
- return true;
- }
+ // Instance of the collection plan
+ protected static $oCollectionPlan;
+
+ public function __construct()
+ {
+ self::$oCollectionPlan = $this;
+ }
+
+ /**
+ * Initialize collection plan
+ *
+ * @return void
+ * @throws \IOException
+ */
+ public function Init(): void
+ {
+ Utils::Log(LOG_INFO, "---------- Build collection plan ----------");
+ }
+
+ /**
+ * @return static
+ */
+ public static function GetPlan()
+ {
+ return self::$oCollectionPlan;
+ }
+
+ /**
+ * Provide the launch sequence as defined in the configuration files
+ *
+ * @return array
+ * @throws \Exception
+ */
+ public function GetSortedLaunchSequence(): array
+ {
+ $aCollectorsLaunchSequence = Utils::GetConfigurationValue('collectors_launch_sequence', []);
+ $aExtensionsCollectorsLaunchSequence = Utils::GetConfigurationValue('extensions_collectors_launch_sequence', []);
+ $aCollectorsLaunchSequence = array_merge($aCollectorsLaunchSequence, $aExtensionsCollectorsLaunchSequence);
+ $aRank = [];
+ if (!empty($aCollectorsLaunchSequence)) {
+ // Sort sequence
+ $aSortedCollectorsLaunchSequence = [];
+ foreach ($aCollectorsLaunchSequence as $aCollector) {
+ if (array_key_exists('rank', $aCollector)) {
+ $aRank[] = $aCollector['rank'];
+ $aSortedCollectorsLaunchSequence[] = $aCollector;
+ } else {
+ Utils::Log(LOG_INFO, "> Rank is missing from the launch_sequence of ".$aCollector['name']." It will not be launched.");
+ }
+ }
+ array_multisort($aRank, SORT_ASC, $aSortedCollectorsLaunchSequence);
+
+ return $aSortedCollectorsLaunchSequence;
+ }
+
+ return $aCollectorsLaunchSequence;
+ }
+
+ /**
+ * Look for the collector definition file in the different possible collector directories
+ *
+ * @param $sCollector
+ *
+ * @return bool
+ */
+ public function GetCollectorDefinitionFile($sCollector): bool
+ {
+ if (file_exists(APPROOT.'collectors/extensions/src/'.$sCollector.'.class.inc.php')) {
+ require_once(APPROOT.'collectors/extensions/src/'.$sCollector.'.class.inc.php');
+ } elseif (file_exists(APPROOT.'collectors/src/'.$sCollector.'.class.inc.php')) {
+ require_once(APPROOT.'collectors/src/'.$sCollector.'.class.inc.php');
+ } elseif (file_exists(APPROOT.'collectors/'.$sCollector.'.class.inc.php')) {
+ require_once(APPROOT.'collectors/'.$sCollector.'.class.inc.php');
+ } else {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Add the collectors to be launched to the orchestrator
+ *
+ * @return bool
+ * @throws \Exception
+ */
+ public function AddCollectorsToOrchestrator(): bool
+ {
+ // Read and order launch sequence
+ $aCollectorsLaunchSequence = $this->GetSortedLaunchSequence();
+ if (empty($aCollectorsLaunchSequence)) {
+ Utils::Log(LOG_INFO, "---------- No Launch sequence has been found, no collector has been orchestrated ----------");
+
+ return false;
+ }
+
+ $iIndex = 1;
+ $aOrchestratedCollectors = [];
+ foreach ($aCollectorsLaunchSequence as $iKey => $aCollector) {
+ $sCollectorName = $aCollector['name'];
+
+ // Skip disabled collectors
+ if (!array_key_exists('enable', $aCollector) || ($aCollector['enable'] != 'yes')) {
+ Utils::Log(LOG_INFO, "> ".$sCollectorName." is disabled and will not be launched.");
+ continue;
+ }
+
+ // Read collector php definition file
+ if (!$this->GetCollectorDefinitionFile($sCollectorName)) {
+ Utils::Log(LOG_INFO, "> No file definition file has been found for ".$sCollectorName." It will not be launched.");
+ continue;
+ }
+
+ /** @var Collector $oCollector */
+ // Instantiate collector
+ $oCollector = new $sCollectorName();
+ $oCollector->Init();
+ if ($oCollector->CheckToLaunch($aOrchestratedCollectors)) {
+ Utils::Log(LOG_INFO, $sCollectorName.' will be launched !');
+ Orchestrator::AddCollector($iIndex++, $sCollectorName);
+ $aOrchestratedCollectors[$sCollectorName] = true;
+ } else {
+ $aOrchestratedCollectors[$sCollectorName] = false;
+ }
+ unset($oCollector);
+ }
+ Utils::Log(LOG_INFO, "---------- Collectors have been orchestrated ----------");
+
+ return true;
+ }
}
diff --git a/core/collector.class.inc.php b/core/collector.class.inc.php
index cfac4f0..4a14e08 100644
--- a/core/collector.class.inc.php
+++ b/core/collector.class.inc.php
@@ -1,4 +1,5 @@
sVersion = null;
- $this->iSourceId = null;
- $this->aFields = [];
- $this->aCSVHeaders = [];
- $this->aCSVFile = array();
- $this->iFileIndex = null;
- $this->aCollectorConfig = [];
- $this->sErrorMessage = '';
- $this->sSeparator = ';';
- $this->aSkippedAttributes = [];
- }
-
- /**
- * @since 1.3.0
- */
- public static function SetCallItopService(CallItopService $oCurrentCallItopService){
- static::$oCallItopService = $oCurrentCallItopService;
- }
-
-
- /**
- * Initialization
- *
- * @return void
- * @throws \Exception
- */
- public function Init(): void
- {
- $sJSONSourceDefinition = $this->GetSynchroDataSourceDefinition();
- if (empty($sJSONSourceDefinition)) {
- Utils::Log(LOG_ERR,
- sprintf("Empty Synchro Data Source definition for the collector '%s' (file to check/create: %s)",
- $this->GetName(),
- $this->sSynchroDataSourceDefinitionFile)
- );
- throw new Exception('Cannot create Collector (empty JSON definition)');
- }
- $aSourceDefinition = json_decode($sJSONSourceDefinition, true);
-
- if ($aSourceDefinition === null) {
- Utils::Log(LOG_ERR, "Invalid Synchro Data Source definition for the collector '".$this->GetName()."' (not a JSON string)");
- throw new Exception('Cannot create Collector (invalid JSON definition)');
- }
- foreach ($aSourceDefinition['attribute_list'] as $aAttr) {
- $aColumns = isset($aAttr['import_columns']) ? explode(',', $aAttr['import_columns']) : [$aAttr['attcode']];
- $this->aFields[$aAttr['attcode']] = ['class' => $aAttr['finalclass'], 'update' => ($aAttr['update'] != 0), 'reconcile' => ($aAttr['reconcile'] != 0), 'columns' => $aColumns];
- }
-
- $this->ReadCollectorConfig();
- if (array_key_exists('nullified_attributes', $this->aCollectorConfig)){
- $this->aNullifiedAttributes = $this->aCollectorConfig['nullified_attributes'];
- } else {
- $this->aNullifiedAttributes = Utils::GetConfigurationValue(get_class($this)."_nullified_attributes", null);
-
- if ($this->aNullifiedAttributes === null) {
- // Try all lowercase
- $this->aNullifiedAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_nullified_attributes", []);
- }
- }
- }
-
- public function ReadCollectorConfig() {
- $this->aCollectorConfig = Utils::GetConfigurationValue(get_class($this), []);
- if (empty($this->aCollectorConfig)) {
- $this->aCollectorConfig = Utils::GetConfigurationValue(strtolower(get_class($this)), []);
- }
- Utils::Log(LOG_DEBUG,
- sprintf("aCollectorConfig %s: [%s]",
- get_class($this),
- json_encode($this->aCollectorConfig)
- )
- );
- }
-
- public function GetErrorMessage()
- {
- return $this->sErrorMessage;
- }
-
- public function GetProjectName()
- {
- return $this->sProjectName;
- }
-
- protected function Fetch()
- {
- // Implement your own mechanism, unless you completely overload Collect()
- }
-
- protected function Prepare()
- {
- $this->RemoveDataFiles();
- $this->sSeparator = ';';
-
- return true;
- }
-
- protected function Cleanup()
- {
- if ($this->iFileIndex !== null) {
- fclose($this->aCSVFile[$this->iFileIndex]);
- }
- }
-
- /*
- * Look for the synchro data source definition file in the different possible collector directories
- *
- * @return false|string
- */
- public function GetSynchroDataSourceDefinitionFile()
- {
- if (file_exists(APPROOT.'collectors/extensions/json/'.get_class($this).'.json')) {
- return APPROOT.'collectors/extensions/json/'.get_class($this).'.json';
- } elseif (file_exists(APPROOT.'collectors/json/'.get_class($this).'.json')) {
- return APPROOT.'collectors/json/'.get_class($this).'.json';
- } elseif (file_exists(APPROOT.'collectors/'.get_class($this).'.json')) {
- return APPROOT.'collectors/'.get_class($this).'.json';
- } else {
- return false;
- }
- }
-
- public function GetSynchroDataSourceDefinition($aPlaceHolders = array())
- {
- $this->sSynchroDataSourceDefinitionFile = $this->GetSynchroDataSourceDefinitionFile();
- if ($this->sSynchroDataSourceDefinitionFile === false) {
- return false;
- }
-
- $aPlaceHolders['$version$'] = $this->GetVersion();
- $sSynchroDataSourceDefinition = file_get_contents($this->sSynchroDataSourceDefinitionFile);
- $sSynchroDataSourceDefinition = str_replace(array_keys($aPlaceHolders), array_values($aPlaceHolders), $sSynchroDataSourceDefinition);
-
- return $sSynchroDataSourceDefinition;
- }
-
- /**
- * Determine if a given attribute can be missing in the data datamodel.
- *
- * Overload this method to let your collector adapt to various datamodels. If an attribute is skipped,
- * its name is recorded in the member variable $this->aSkippedAttributes for later reference.
- *
- * @param string $sAttCode
- *
- * @return boolean True if the attribute can be skipped, false otherwise
- */
- public function AttributeIsOptional($sAttCode)
- {
- return false; // By default no attribute is optional
- }
-
- /**
- * Determine if a given attribute null value is allowed to be transformed between collect and data synchro steps.
- *
- * If transformed, its null value is replaced by '' and sent to iTop data synchro.
- * It means that existing value on iTop side will be kept as is.
- *
- * Otherwise if not transformed, empty string value will be sent to datasynchro which means resetting current value on iTop side.
- *
- * Best practice:
- * for fields like decimal, integer or enum, we recommend to configure transformation as resetting will fail on iTop side (Bug N°776).
- *
- * The implementation is based on a predefined configuration parameter named from the
- * class of the collector (all lowercase) with _nullified_attributes appended.
- *
- * Example: here is the configuration to "nullify" the attribute 'location_id' for the class MyCollector:
- *
- * location_id
- *
- *
- * @param string $sAttCode
- *
- * @return boolean True if the attribute can be skipped, false otherwise
- */
- public function AttributeIsNullified($sAttCode)
- {
- if (is_array($this->aNullifiedAttributes)) {
- return in_array($sAttCode, $this->aNullifiedAttributes);
- }
-
- return false;
- }
-
- public function GetName()
- {
- return get_class($this);
- }
-
- public function GetVersion()
- {
- if ($this->sVersion == null) {
- $this->GetVersionFromModuleFile();
- }
-
- return $this->sVersion;
- }
-
- /**
- * Overload this method (to return true) if the collector has
- * to reprocess the CSV file (with an access to iTop)
- * before executing the synchro with iTop
- *
- * @return bool
- */
- protected function MustProcessBeforeSynchro()
- {
- // Overload this method (to return true) if the collector has
- // to reprocess the CSV file (with an access to iTop)
- // before executing the synchro with iTop
- return false;
- }
-
- /**
- * Overload this method to perform any one-time initialization which
- * may be required before processing the CSV file line by line
- *
- * @return void
- */
- protected function InitProcessBeforeSynchro()
- {
- }
-
- /**
- * Overload this method to process each line of the CSV file
- * Should you need to "reject" the line from the ouput, throw an exception of class IgnoredRowException
- * Example:
- * throw new IgnoredRowException('Explain why the line is rejected - visible in the debug output');
- *
- * @param array $aLineData
- * @param int $iLineIndex
- *
- * @return void
- * @throws IgnoredRowException
- */
- protected function ProcessLineBeforeSynchro(&$aLineData, $iLineIndex)
- {
- }
-
- protected function DoProcessBeforeSynchro()
- {
- $this->InitProcessBeforeSynchro();
-
- $aFiles = glob(Utils::GetDataFilePath(get_class($this).'.raw-*.csv'));
- foreach ($aFiles as $sDataFile) {
- Utils::Log(LOG_INFO, "Processing '$sDataFile'...");
- // Warning backslashes inside the file path (like C:\ on Windows) must be escaped (i.e. \ => \\), but inside a PHP string \ is written '\\' so \\ becomes '\\\\' !!
- $sPattern = '|'.str_replace('\\', '\\\\', Utils::GetDataFilePath(get_class($this))).'\\.raw-([0-9]+)\\.csv$|';
- if (preg_match($sPattern, $sDataFile, $aMatches)) {
- $idx = $aMatches[1];
- $sOutputFile = Utils::GetDataFilePath(get_class($this).'-'.$idx.'.csv');
- Utils::Log(LOG_DEBUG, "Converting '$sDataFile' to '$sOutputFile'...");
-
- $hCSV = fopen($sDataFile, 'r');
- if ($hCSV === false) {
- Utils::Log(LOG_ERR, "Failed to open '$sDataFile' for reading... file will be skipped.");
- }
-
- $hOutputCSV = fopen($sOutputFile, 'w');
- if ($hOutputCSV === false) {
- Utils::Log(LOG_ERR, "Failed to open '$sOutputFile' for writing... file will be skipped.");
- }
-
- if (($hCSV !== false) && ($hOutputCSV !== false)) {
- $iLineIndex = 0;
- while (($aData = fgetcsv($hCSV, 10000, $this->sSeparator)) !== false) {
- //process
- try {
- $this->ProcessLineBeforeSynchro($aData, $iLineIndex);
- // Write the CSV data
- fputcsv($hOutputCSV, $aData, $this->sSeparator);
- } catch (IgnoredRowException $e) {
- // Skip this line
- Utils::Log(LOG_DEBUG, "Ignoring the line $iLineIndex. Reason: ".$e->getMessage());
- }
- $iLineIndex++;
- }
- fclose($hCSV);
- fclose($hOutputCSV);
- Utils::Log(LOG_INFO, "End of processing of '$sDataFile'...");
- }
- } else {
- Utils::Log(LOG_DEBUG, "'$sDataFile' does not match '$sPattern'... file will be skipped.");
- }
- }
- }
-
- /**
- * Overload this method if the data collected is in a different character set
- *
- * @return string The name of the character set in which the collected data are encoded
- */
- protected function GetCharset()
- {
- return 'UTF-8';
- }
-
- /////////////////////////////////////////////////////////////////////////
-
- /**
- * Extracts the version number by evaluating the content of the first module.xxx.php file found
- * in the "collector" directory.
- */
- protected function GetVersionFromModuleFile()
- {
- $aFiles = glob(APPROOT.'collectors/module.*.php');
- if (!$aFiles) {
- // No module found, use a default value...
- $this->sVersion = '1.0.0';
- Utils::Log(LOG_INFO, "Please create a 'module.*.php' file in 'collectors' folder in order to define the version of collectors");
-
- return;
- }
-
- try {
- $sModuleFile = reset($aFiles);
-
- $this->SetProjectNameFromFileName($sModuleFile);
-
- $sModuleFileContents = file_get_contents($sModuleFile);
- $sModuleFileContents = str_replace(array(''), '', $sModuleFileContents);
- $sModuleFileContents = str_replace('SetupWebPage::AddModule(', '$this->InitFieldsFromModuleContentCallback(', $sModuleFileContents);
- $bRet = eval($sModuleFileContents);
-
- if ($bRet === false) {
- Utils::Log(LOG_WARNING, "Eval of '$sModuleFileContents' returned false");
- }
- } catch (Exception $e) {
- // Continue...
- Utils::Log(LOG_WARNING, "Eval of '$sModuleFileContents' caused an exception: ".$e->getMessage());
- }
- }
-
- /**
- * @param string $sModuleFile
- */
- public function SetProjectNameFromFileName($sModuleFile)
- {
- if (preg_match("/module\.(.*)\.php/", $sModuleFile, $aMatches)) {
- $this->SetProjectName($aMatches[1]);
- }
- }
-
- /**
- * @param string $sProjectName
- */
- public function SetProjectName($sProjectName): void
- {
- $this->sProjectName = $sProjectName;
- Utils::SetProjectName($sProjectName);
- }
-
-
- /**
- * Sets the $sVersion property. Called when eval'uating the content of the module file
- *
- * @param string $void1 Unused
- * @param string $sId The identifier of the module. Format: 'name/version'
- * @param array $void2 Unused
- */
- protected function InitFieldsFromModuleContentCallback($void1, $sId, $void2)
- {
- if (preg_match('!^(.*)/(.*)$!', $sId, $aMatches)) {
- $this->SetProjectName($aMatches[1]);
- $this->sVersion = $aMatches[2];
- } else {
- $this->sVersion = "1.0.0";
- }
- }
-
- /**
- * Inspects the definition of the Synchro Data Source to find inconsistencies
- *
- * @param mixed[] $aExpectedSourceDefinition
- *
- * @return void
- * @throws Exception
- */
- protected function CheckDataSourceDefinition($aExpectedSourceDefinition)
- {
- Utils::Log(LOG_DEBUG, "Checking the configuration of the data source '{$aExpectedSourceDefinition['name']}'...");
-
- // Check that there is at least 1 reconciliation key, if the reconciliation_policy is "use_attributes"
- if ($aExpectedSourceDefinition['reconciliation_policy'] == 'use_attributes') {
- $bReconciliationKeyFound = false;
- foreach ($aExpectedSourceDefinition['attribute_list'] as $aAttributeDef) {
- if ($aAttributeDef['reconcile'] == '1') {
- $bReconciliationKeyFound = true;
- break;
- }
- }
- if (!$bReconciliationKeyFound) {
- throw new InvalidConfigException("Collector::CheckDataSourceDefinition: Missing reconciliation key for data source '{$aExpectedSourceDefinition['name']}'. ".
- "At least one attribute in 'attribute_list' must have the flag 'reconcile' set to '1'.");
- }
- }
-
- // Check the database table name for invalid characters
- $sDatabaseTableName = $aExpectedSourceDefinition['database_table_name'];
- if (!preg_match(self::TABLENAME_PATTERN, $sDatabaseTableName)) {
- throw new InvalidConfigException("Collector::CheckDataSourceDefinition: '{$aExpectedSourceDefinition['name']}' invalid characters in database_table_name, ".
- "current value is '$sDatabaseTableName'");
- }
-
- Utils::Log(LOG_DEBUG, "The configuration of the data source '{$aExpectedSourceDefinition['name']}' looks correct.");
- }
-
- public function InitSynchroDataSource($aPlaceholders)
- {
- $bResult = true;
- $sJSONSourceDefinition = $this->GetSynchroDataSourceDefinition($aPlaceholders);
- $aExpectedSourceDefinition = json_decode($sJSONSourceDefinition, true);
- $this->CheckDataSourceDefinition($aExpectedSourceDefinition);
-
- $this->sSourceName = $aExpectedSourceDefinition['name'];
- try {
- $oRestClient = new RestClient();
- $aResult = $oRestClient->Get('SynchroDataSource', array('name' => $this->sSourceName));
- if ($aResult['code'] != 0) {
- Utils::Log(LOG_ERR, "{$aResult['message']} ({$aResult['code']})");
- $bResult = false;
- } else {
- $iCount = ($aResult['objects'] !== null) ? count($aResult['objects']) : 0;
- switch ($iCount) {
- case 0:
- // not found, need to create the Source
- Utils::Log(LOG_INFO, "There is no Synchro Data Source named '{$this->sSourceName}' in iTop. Let's create it.");
- $key = $this->CreateSynchroDataSource($aExpectedSourceDefinition, $this->GetName());
- if ($key === false) {
- $bResult = false;
- }
- break;
-
- case 1:
- foreach ($aResult['objects'] as $sKey => $aData) {
- // Ok, found, is it up to date ?
- $aData = reset($aResult['objects']);
- $aCurrentSourceDefinition = $aData['fields'];
- $iKey=0;
- if (!array_key_exists('key', $aData)) {
- // Emulate the behavior for older versions of the API
- if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
- $iKey = (int)$aMatches[1];
- }
- } else {
- $iKey = (int)$aData['key'];
- }
- $this->iSourceId = $iKey;
- RestClient::GetFullSynchroDataSource($aCurrentSourceDefinition, $this->iSourceId);
- if ($this->DataSourcesAreEquivalent($aExpectedSourceDefinition, $aCurrentSourceDefinition)) {
- Utils::Log(LOG_INFO, "Ok, the Synchro Data Source '{$this->sSourceName}' exists in iTop and is up to date");
- } else {
- Utils::Log(LOG_INFO, "The Synchro Data Source definition for '{$this->sSourceName}' must be updated in iTop.");
- if (LOG_DEBUG <= Utils::$iConsoleLogLevel) {
- file_put_contents(APPROOT.'data/tmp-'.get_class($this).'-orig.txt', print_r($aExpectedSourceDefinition, true));
- file_put_contents(APPROOT.'data/tmp-'.get_class($this).'-itop.txt', print_r($aCurrentSourceDefinition, true));
- }
- $bResult = $this->UpdateSynchroDataSource($aExpectedSourceDefinition, $this->GetName());
- }
- }
- break;
-
- default:
- // Ambiguous !!
- Utils::Log(LOG_ERR, "There are ".count($aResult['objects'])." Synchro Data Sources named '{$this->sSourceName}' in iTop. Cannot continue.");
- $bResult = false;
- }
- }
- } catch (Exception $e) {
- Utils::Log(LOG_ERR, $e->getMessage());
- $bResult = false;
- }
-
- return $bResult;
- }
-
- public function Collect($iMaxChunkSize = 0)
- {
- Utils::Log(LOG_INFO, get_class($this)." beginning of data collection...");
- try {
- $bResult = $this->Prepare();
- if ($bResult) {
- $idx = 0;
- $aHeaders = null;
- while ($aRow = $this->Fetch()) {
- if ($aHeaders == null) {
- // Check that the row names are consistent with the definition of the task
- $aHeaders = array_keys($aRow);
- }
-
- if (($idx == 0) || (($iMaxChunkSize > 0) && (($idx % $iMaxChunkSize) == 0))) {
- $this->NextCSVFile();
- $this->AddHeader($aHeaders);
- }
-
- $this->AddRow($aRow);
-
- $idx++;
- }
- $this->Cleanup();
- Utils::Log(LOG_INFO, get_class($this)." end of data collection.");
- } else {
- Utils::Log(LOG_ERR, get_class($this)."::Prepare() returned false");
- }
- } catch (Exception $e) {
- $bResult = false;
- Utils::Log(LOG_ERR, get_class($this)."::Collect() got an exception: ".$e->getMessage());
- }
-
- return $bResult;
- }
-
- protected function AddHeader($aHeaders)
- {
- $this->aCSVHeaders = array();
- foreach ($aHeaders as $sHeader) {
- if (($sHeader != 'primary_key') && !$this->HeaderIsAllowed($sHeader)) {
- if (!$this->AttributeIsOptional($sHeader)) {
- Utils::Log(LOG_WARNING, "Invalid column '$sHeader', will be ignored.");
- }
- } else {
- $this->aCSVHeaders[] = $sHeader;
- }
- }
- fputcsv($this->aCSVFile[$this->iFileIndex], $this->aCSVHeaders, $this->sSeparator);
- }
-
- /**
- * Added to add multi-column field support or situations
- * where column name is different than attribute code.
- *
- * @param string $sHeader
- * @return bool
- */
- protected function HeaderIsAllowed($sHeader)
- {
- foreach ($this->aFields as $aField) {
- if (in_array($sHeader, $aField['columns'])) return true;
- }
-
- // fallback old behaviour
- return array_key_exists($sHeader, $this->aFields);
- }
-
- protected function AddRow($aRow)
- {
- $aData = array();
- foreach ($this->aCSVHeaders as $sHeader) {
- if (is_null($aRow[$sHeader]) && $this->AttributeIsNullified($sHeader)) {
- $aData[] = NULL_VALUE;
- } else {
- $aData[] = $aRow[$sHeader];
- }
- }
- //fwrite($this->aCSVFile[$this->iFileIndex], implode($this->sSeparator, $aData)."\n");
- fputcsv($this->aCSVFile[$this->iFileIndex], $aData, $this->sSeparator);
- }
-
- /**
- * @return true
- * @throws IOException When file cannot be opened.
- */
- protected function OpenCSVFile()
- {
- if ($this->MustProcessBeforeSynchro()) {
- $sDataFile = Utils::GetDataFilePath(get_class($this).'.raw-'.(1 + $this->iFileIndex).'.csv');
- } else {
- $sDataFile = Utils::GetDataFilePath(get_class($this).'-'.(1 + $this->iFileIndex).'.csv');
- }
- $this->aCSVFile[$this->iFileIndex] = @fopen($sDataFile, 'wb');
-
- if ($this->aCSVFile[$this->iFileIndex] === false) {
- throw new IOException("Unable to open the file '$sDataFile' for writing.");
- } else {
- Utils::Log(LOG_INFO, "Writing to file '$sDataFile'.");
- }
-
- return true;
- }
-
- /**
- * @return true
- * @throws IOException When file cannot be opened.
- */
- protected function NextCSVFile()
- {
- if ($this->iFileIndex !== null) {
- fclose($this->aCSVFile[$this->iFileIndex]);
- $this->aCSVFile[$this->iFileIndex] = false;
- $this->iFileIndex++;
- } else {
- $this->iFileIndex = 0;
- }
-
- return $this->OpenCSVFile();
- }
-
- protected function RemoveDataFiles()
- {
- $aFiles = glob(Utils::GetDataFilePath(get_class($this).'-*.csv'));
- foreach ($aFiles as $sFile) {
- $bResult = @unlink($sFile);
- Utils::Log(LOG_DEBUG, "Erasing previous data file. unlink('$sFile') returned ".($bResult ? 'true' : 'false'));
- }
- $aFiles = glob(Utils::GetDataFilePath(get_class($this).'.raw-*.csv'));
- foreach ($aFiles as $sFile) {
- $bResult = @unlink($sFile);
- Utils::Log(LOG_DEBUG, "Erasing previous data file. unlink('$sFile') returned ".($bResult ? 'true' : 'false'));
- }
- }
-
- public function Synchronize($iMaxChunkSize = 0)
- {
- // Let a chance to the collector to alter/reprocess the CSV file
- // before running the synchro. This is useful for performing advanced lookups
- // in iTop, for example for some Typology with several cascading levels that
- // the data synchronization can not handle directly
- if ($this->MustProcessBeforeSynchro()) {
- $this->DoProcessBeforeSynchro();
- }
-
- $aFiles = glob(Utils::GetDataFilePath(get_class($this).'-*.csv'));
-
- //use synchro detailed output for log level 7
- //explanation: there is a weard behaviour with LOG level under windows (some PHP versions??)
- // under linux LOG_DEBUG=7 and LOG_INFO=6
- // under windows LOG_DEBUG=LOG_INFO=6...
- $bDetailedOutput = ("7" === "" . Utils::$iConsoleLogLevel);
- foreach ($aFiles as $sDataFile) {
- Utils::Log(LOG_INFO, "Uploading data file '$sDataFile'");
- // Load by chunk
-
- if ($bDetailedOutput) {
- $sOutput = 'details';
- } else {
- $sOutput = 'retcode';
- }
-
- $aData = array(
- 'separator' => ';',
- 'data_source_id' => $this->iSourceId,
- 'synchronize' => '0',
- 'no_stop_on_import_error' => 1,
- 'output' => $sOutput,
- 'csvdata' => file_get_contents($sDataFile),
- 'charset' => $this->GetCharset(),
+ /**
+ * @see N°2417
+ * @var string TABLENAME_PATTERN used to validate data synchro table name
+ */
+ public const TABLENAME_PATTERN = '/^[A-Za-z0-9_]*$/';
+
+ protected $sProjectName;
+ protected $sSynchroDataSourceDefinitionFile;
+ protected $sVersion;
+ protected $iSourceId;
+ protected $aFields;
+ protected $aCSVHeaders;
+ protected $aCSVFile;
+ protected $iFileIndex;
+ protected $aCollectorConfig;
+ protected $sErrorMessage;
+ protected $sSeparator;
+ protected $aSkippedAttributes;
+ protected $aNullifiedAttributes;
+ protected $sSourceName;
+
+ /** @var \CallItopService $oCallItopService */
+ protected static $oCallItopService;
+
+ /**
+ * Construction
+ */
+ public function __construct()
+ {
+ $this->sVersion = null;
+ $this->iSourceId = null;
+ $this->aFields = [];
+ $this->aCSVHeaders = [];
+ $this->aCSVFile = [];
+ $this->iFileIndex = null;
+ $this->aCollectorConfig = [];
+ $this->sErrorMessage = '';
+ $this->sSeparator = ';';
+ $this->aSkippedAttributes = [];
+ }
+
+ /**
+ * @since 1.3.0
+ */
+ public static function SetCallItopService(CallItopService $oCurrentCallItopService)
+ {
+ static::$oCallItopService = $oCurrentCallItopService;
+ }
+
+ /**
+ * Initialization
+ *
+ * @return void
+ * @throws \Exception
+ */
+ public function Init(): void
+ {
+ $sJSONSourceDefinition = $this->GetSynchroDataSourceDefinition();
+ if (empty($sJSONSourceDefinition)) {
+ Utils::Log(
+ LOG_ERR,
+ sprintf(
+ "Empty Synchro Data Source definition for the collector '%s' (file to check/create: %s)",
+ $this->GetName(),
+ $this->sSynchroDataSourceDefinitionFile
+ )
+ );
+ throw new Exception('Cannot create Collector (empty JSON definition)');
+ }
+ $aSourceDefinition = json_decode($sJSONSourceDefinition, true);
+
+ if ($aSourceDefinition === null) {
+ Utils::Log(LOG_ERR, "Invalid Synchro Data Source definition for the collector '".$this->GetName()."' (not a JSON string)");
+ throw new Exception('Cannot create Collector (invalid JSON definition)');
+ }
+ foreach ($aSourceDefinition['attribute_list'] as $aAttr) {
+ $aColumns = isset($aAttr['import_columns']) ? explode(',', $aAttr['import_columns']) : [$aAttr['attcode']];
+ $this->aFields[$aAttr['attcode']] = ['class' => $aAttr['finalclass'], 'update' => ($aAttr['update'] != 0), 'reconcile' => ($aAttr['reconcile'] != 0), 'columns' => $aColumns];
+ }
+
+ $this->ReadCollectorConfig();
+ if (array_key_exists('nullified_attributes', $this->aCollectorConfig)) {
+ $this->aNullifiedAttributes = $this->aCollectorConfig['nullified_attributes'];
+ } else {
+ $this->aNullifiedAttributes = Utils::GetConfigurationValue(get_class($this)."_nullified_attributes", null);
+
+ if ($this->aNullifiedAttributes === null) {
+ // Try all lowercase
+ $this->aNullifiedAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_nullified_attributes", []);
+ }
+ }
+ }
+
+ public function ReadCollectorConfig()
+ {
+ $this->aCollectorConfig = Utils::GetConfigurationValue(get_class($this), []);
+ if (empty($this->aCollectorConfig)) {
+ $this->aCollectorConfig = Utils::GetConfigurationValue(strtolower(get_class($this)), []);
+ }
+ Utils::Log(
+ LOG_DEBUG,
+ sprintf(
+ "aCollectorConfig %s: [%s]",
+ get_class($this),
+ json_encode($this->aCollectorConfig)
+ )
+ );
+ }
+
+ public function GetErrorMessage()
+ {
+ return $this->sErrorMessage;
+ }
+
+ public function GetProjectName()
+ {
+ return $this->sProjectName;
+ }
+
+ protected function Fetch()
+ {
+ // Implement your own mechanism, unless you completely overload Collect()
+ }
+
+ protected function Prepare()
+ {
+ $this->RemoveDataFiles();
+ $this->sSeparator = ';';
+
+ return true;
+ }
+
+ protected function Cleanup()
+ {
+ if ($this->iFileIndex !== null) {
+ fclose($this->aCSVFile[$this->iFileIndex]);
+ }
+ }
+
+ /*
+ * Look for the synchro data source definition file in the different possible collector directories
+ *
+ * @return false|string
+ */
+ public function GetSynchroDataSourceDefinitionFile()
+ {
+ if (file_exists(APPROOT.'collectors/extensions/json/'.get_class($this).'.json')) {
+ return APPROOT.'collectors/extensions/json/'.get_class($this).'.json';
+ } elseif (file_exists(APPROOT.'collectors/json/'.get_class($this).'.json')) {
+ return APPROOT.'collectors/json/'.get_class($this).'.json';
+ } elseif (file_exists(APPROOT.'collectors/'.get_class($this).'.json')) {
+ return APPROOT.'collectors/'.get_class($this).'.json';
+ } else {
+ return false;
+ }
+ }
+
+ public function GetSynchroDataSourceDefinition($aPlaceHolders = [])
+ {
+ $this->sSynchroDataSourceDefinitionFile = $this->GetSynchroDataSourceDefinitionFile();
+ if ($this->sSynchroDataSourceDefinitionFile === false) {
+ return false;
+ }
+
+ $aPlaceHolders['$version$'] = $this->GetVersion();
+ $sSynchroDataSourceDefinition = file_get_contents($this->sSynchroDataSourceDefinitionFile);
+ $sSynchroDataSourceDefinition = str_replace(array_keys($aPlaceHolders), array_values($aPlaceHolders), $sSynchroDataSourceDefinition);
+
+ return $sSynchroDataSourceDefinition;
+ }
+
+ /**
+ * Determine if a given attribute can be missing in the data datamodel.
+ *
+ * Overload this method to let your collector adapt to various datamodels. If an attribute is skipped,
+ * its name is recorded in the member variable $this->aSkippedAttributes for later reference.
+ *
+ * @param string $sAttCode
+ *
+ * @return boolean True if the attribute can be skipped, false otherwise
+ */
+ public function AttributeIsOptional($sAttCode)
+ {
+ return false; // By default no attribute is optional
+ }
+
+ /**
+ * Determine if a given attribute null value is allowed to be transformed between collect and data synchro steps.
+ *
+ * If transformed, its null value is replaced by '' and sent to iTop data synchro.
+ * It means that existing value on iTop side will be kept as is.
+ *
+ * Otherwise if not transformed, empty string value will be sent to datasynchro which means resetting current value on iTop side.
+ *
+ * Best practice:
+ * for fields like decimal, integer or enum, we recommend to configure transformation as resetting will fail on iTop side (Bug N°776).
+ *
+ * The implementation is based on a predefined configuration parameter named from the
+ * class of the collector (all lowercase) with _nullified_attributes appended.
+ *
+ * Example: here is the configuration to "nullify" the attribute 'location_id' for the class MyCollector:
+ *
+ * location_id
+ *
+ *
+ * @param string $sAttCode
+ *
+ * @return boolean True if the attribute can be skipped, false otherwise
+ */
+ public function AttributeIsNullified($sAttCode)
+ {
+ if (is_array($this->aNullifiedAttributes)) {
+ return in_array($sAttCode, $this->aNullifiedAttributes);
+ }
+
+ return false;
+ }
+
+ public function GetName()
+ {
+ return get_class($this);
+ }
+
+ public function GetVersion()
+ {
+ if ($this->sVersion == null) {
+ $this->GetVersionFromModuleFile();
+ }
+
+ return $this->sVersion;
+ }
+
+ /**
+ * Overload this method (to return true) if the collector has
+ * to reprocess the CSV file (with an access to iTop)
+ * before executing the synchro with iTop
+ *
+ * @return bool
+ */
+ protected function MustProcessBeforeSynchro()
+ {
+ // Overload this method (to return true) if the collector has
+ // to reprocess the CSV file (with an access to iTop)
+ // before executing the synchro with iTop
+ return false;
+ }
+
+ /**
+ * Overload this method to perform any one-time initialization which
+ * may be required before processing the CSV file line by line
+ *
+ * @return void
+ */
+ protected function InitProcessBeforeSynchro()
+ {
+ }
+
+ /**
+ * Overload this method to process each line of the CSV file
+ * Should you need to "reject" the line from the ouput, throw an exception of class IgnoredRowException
+ * Example:
+ * throw new IgnoredRowException('Explain why the line is rejected - visible in the debug output');
+ *
+ * @param array $aLineData
+ * @param int $iLineIndex
+ *
+ * @return void
+ * @throws IgnoredRowException
+ */
+ protected function ProcessLineBeforeSynchro(&$aLineData, $iLineIndex)
+ {
+ }
+
+ protected function DoProcessBeforeSynchro()
+ {
+ $this->InitProcessBeforeSynchro();
+
+ $aFiles = glob(Utils::GetDataFilePath(get_class($this).'.raw-*.csv'));
+ foreach ($aFiles as $sDataFile) {
+ Utils::Log(LOG_INFO, "Processing '$sDataFile'...");
+ // Warning backslashes inside the file path (like C:\ on Windows) must be escaped (i.e. \ => \\), but inside a PHP string \ is written '\\' so \\ becomes '\\\\' !!
+ $sPattern = '|'.str_replace('\\', '\\\\', Utils::GetDataFilePath(get_class($this))).'\\.raw-([0-9]+)\\.csv$|';
+ if (preg_match($sPattern, $sDataFile, $aMatches)) {
+ $idx = $aMatches[1];
+ $sOutputFile = Utils::GetDataFilePath(get_class($this).'-'.$idx.'.csv');
+ Utils::Log(LOG_DEBUG, "Converting '$sDataFile' to '$sOutputFile'...");
+
+ $hCSV = fopen($sDataFile, 'r');
+ if ($hCSV === false) {
+ Utils::Log(LOG_ERR, "Failed to open '$sDataFile' for reading... file will be skipped.");
+ }
+
+ $hOutputCSV = fopen($sOutputFile, 'w');
+ if ($hOutputCSV === false) {
+ Utils::Log(LOG_ERR, "Failed to open '$sOutputFile' for writing... file will be skipped.");
+ }
+
+ if (($hCSV !== false) && ($hOutputCSV !== false)) {
+ $iLineIndex = 0;
+ while (($aData = fgetcsv($hCSV, 10000, $this->sSeparator)) !== false) {
+ //process
+ try {
+ $this->ProcessLineBeforeSynchro($aData, $iLineIndex);
+ // Write the CSV data
+ fputcsv($hOutputCSV, $aData, $this->sSeparator);
+ } catch (IgnoredRowException $e) {
+ // Skip this line
+ Utils::Log(LOG_DEBUG, "Ignoring the line $iLineIndex. Reason: ".$e->getMessage());
+ }
+ $iLineIndex++;
+ }
+ fclose($hCSV);
+ fclose($hOutputCSV);
+ Utils::Log(LOG_INFO, "End of processing of '$sDataFile'...");
+ }
+ } else {
+ Utils::Log(LOG_DEBUG, "'$sDataFile' does not match '$sPattern'... file will be skipped.");
+ }
+ }
+ }
+
+ /**
+ * Overload this method if the data collected is in a different character set
+ *
+ * @return string The name of the character set in which the collected data are encoded
+ */
+ protected function GetCharset()
+ {
+ return 'UTF-8';
+ }
+
+ /////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Extracts the version number by evaluating the content of the first module.xxx.php file found
+ * in the "collector" directory.
+ */
+ protected function GetVersionFromModuleFile()
+ {
+ $aFiles = glob(APPROOT.'collectors/module.*.php');
+ if (!$aFiles) {
+ // No module found, use a default value...
+ $this->sVersion = '1.0.0';
+ Utils::Log(LOG_INFO, "Please create a 'module.*.php' file in 'collectors' folder in order to define the version of collectors");
+
+ return;
+ }
+
+ try {
+ $sModuleFile = reset($aFiles);
+
+ $this->SetProjectNameFromFileName($sModuleFile);
+
+ $sModuleFileContents = file_get_contents($sModuleFile);
+ $sModuleFileContents = str_replace([''], '', $sModuleFileContents);
+ $sModuleFileContents = str_replace('SetupWebPage::AddModule(', '$this->InitFieldsFromModuleContentCallback(', $sModuleFileContents);
+ $bRet = eval($sModuleFileContents);
+
+ if ($bRet === false) {
+ Utils::Log(LOG_WARNING, "Eval of '$sModuleFileContents' returned false");
+ }
+ } catch (Exception $e) {
+ // Continue...
+ Utils::Log(LOG_WARNING, "Eval of '$sModuleFileContents' caused an exception: ".$e->getMessage());
+ }
+ }
+
+ /**
+ * @param string $sModuleFile
+ */
+ public function SetProjectNameFromFileName($sModuleFile)
+ {
+ if (preg_match("/module\.(.*)\.php/", $sModuleFile, $aMatches)) {
+ $this->SetProjectName($aMatches[1]);
+ }
+ }
+
+ /**
+ * @param string $sProjectName
+ */
+ public function SetProjectName($sProjectName): void
+ {
+ $this->sProjectName = $sProjectName;
+ Utils::SetProjectName($sProjectName);
+ }
+
+ /**
+ * Sets the $sVersion property. Called when eval'uating the content of the module file
+ *
+ * @param string $void1 Unused
+ * @param string $sId The identifier of the module. Format: 'name/version'
+ * @param array $void2 Unused
+ */
+ protected function InitFieldsFromModuleContentCallback($void1, $sId, $void2)
+ {
+ if (preg_match('!^(.*)/(.*)$!', $sId, $aMatches)) {
+ $this->SetProjectName($aMatches[1]);
+ $this->sVersion = $aMatches[2];
+ } else {
+ $this->sVersion = "1.0.0";
+ }
+ }
+
+ /**
+ * Inspects the definition of the Synchro Data Source to find inconsistencies
+ *
+ * @param mixed[] $aExpectedSourceDefinition
+ *
+ * @return void
+ * @throws Exception
+ */
+ protected function CheckDataSourceDefinition($aExpectedSourceDefinition)
+ {
+ Utils::Log(LOG_DEBUG, "Checking the configuration of the data source '{$aExpectedSourceDefinition['name']}'...");
+
+ // Check that there is at least 1 reconciliation key, if the reconciliation_policy is "use_attributes"
+ if ($aExpectedSourceDefinition['reconciliation_policy'] == 'use_attributes') {
+ $bReconciliationKeyFound = false;
+ foreach ($aExpectedSourceDefinition['attribute_list'] as $aAttributeDef) {
+ if ($aAttributeDef['reconcile'] == '1') {
+ $bReconciliationKeyFound = true;
+ break;
+ }
+ }
+ if (!$bReconciliationKeyFound) {
+ throw new InvalidConfigException("Collector::CheckDataSourceDefinition: Missing reconciliation key for data source '{$aExpectedSourceDefinition['name']}'. ".
+ "At least one attribute in 'attribute_list' must have the flag 'reconcile' set to '1'.");
+ }
+ }
+
+ // Check the database table name for invalid characters
+ $sDatabaseTableName = $aExpectedSourceDefinition['database_table_name'];
+ if (!preg_match(self::TABLENAME_PATTERN, $sDatabaseTableName)) {
+ throw new InvalidConfigException("Collector::CheckDataSourceDefinition: '{$aExpectedSourceDefinition['name']}' invalid characters in database_table_name, ".
+ "current value is '$sDatabaseTableName'");
+ }
+
+ Utils::Log(LOG_DEBUG, "The configuration of the data source '{$aExpectedSourceDefinition['name']}' looks correct.");
+ }
+
+ public function InitSynchroDataSource($aPlaceholders)
+ {
+ $bResult = true;
+ $sJSONSourceDefinition = $this->GetSynchroDataSourceDefinition($aPlaceholders);
+ $aExpectedSourceDefinition = json_decode($sJSONSourceDefinition, true);
+ $this->CheckDataSourceDefinition($aExpectedSourceDefinition);
+
+ $this->sSourceName = $aExpectedSourceDefinition['name'];
+ try {
+ $oRestClient = new RestClient();
+ $aResult = $oRestClient->Get('SynchroDataSource', ['name' => $this->sSourceName]);
+ if ($aResult['code'] != 0) {
+ Utils::Log(LOG_ERR, "{$aResult['message']} ({$aResult['code']})");
+ $bResult = false;
+ } else {
+ $iCount = ($aResult['objects'] !== null) ? count($aResult['objects']) : 0;
+ switch ($iCount) {
+ case 0:
+ // not found, need to create the Source
+ Utils::Log(LOG_INFO, "There is no Synchro Data Source named '{$this->sSourceName}' in iTop. Let's create it.");
+ $key = $this->CreateSynchroDataSource($aExpectedSourceDefinition, $this->GetName());
+ if ($key === false) {
+ $bResult = false;
+ }
+ break;
+
+ case 1:
+ foreach ($aResult['objects'] as $sKey => $aData) {
+ // Ok, found, is it up to date ?
+ $aData = reset($aResult['objects']);
+ $aCurrentSourceDefinition = $aData['fields'];
+ $iKey = 0;
+ if (!array_key_exists('key', $aData)) {
+ // Emulate the behavior for older versions of the API
+ if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
+ $iKey = (int)$aMatches[1];
+ }
+ } else {
+ $iKey = (int)$aData['key'];
+ }
+ $this->iSourceId = $iKey;
+ RestClient::GetFullSynchroDataSource($aCurrentSourceDefinition, $this->iSourceId);
+ if ($this->DataSourcesAreEquivalent($aExpectedSourceDefinition, $aCurrentSourceDefinition)) {
+ Utils::Log(LOG_INFO, "Ok, the Synchro Data Source '{$this->sSourceName}' exists in iTop and is up to date");
+ } else {
+ Utils::Log(LOG_INFO, "The Synchro Data Source definition for '{$this->sSourceName}' must be updated in iTop.");
+ if (LOG_DEBUG <= Utils::$iConsoleLogLevel) {
+ file_put_contents(APPROOT.'data/tmp-'.get_class($this).'-orig.txt', print_r($aExpectedSourceDefinition, true));
+ file_put_contents(APPROOT.'data/tmp-'.get_class($this).'-itop.txt', print_r($aCurrentSourceDefinition, true));
+ }
+ $bResult = $this->UpdateSynchroDataSource($aExpectedSourceDefinition, $this->GetName());
+ }
+ }
+ break;
+
+ default:
+ // Ambiguous !!
+ Utils::Log(LOG_ERR, "There are ".count($aResult['objects'])." Synchro Data Sources named '{$this->sSourceName}' in iTop. Cannot continue.");
+ $bResult = false;
+ }
+ }
+ } catch (Exception $e) {
+ Utils::Log(LOG_ERR, $e->getMessage());
+ $bResult = false;
+ }
+
+ return $bResult;
+ }
+
+ public function Collect($iMaxChunkSize = 0)
+ {
+ Utils::Log(LOG_INFO, get_class($this)." beginning of data collection...");
+ try {
+ $bResult = $this->Prepare();
+ if ($bResult) {
+ $idx = 0;
+ $aHeaders = null;
+ while ($aRow = $this->Fetch()) {
+ if ($aHeaders == null) {
+ // Check that the row names are consistent with the definition of the task
+ $aHeaders = array_keys($aRow);
+ }
+
+ if (($idx == 0) || (($iMaxChunkSize > 0) && (($idx % $iMaxChunkSize) == 0))) {
+ $this->NextCSVFile();
+ $this->AddHeader($aHeaders);
+ }
+
+ $this->AddRow($aRow);
+
+ $idx++;
+ }
+ $this->Cleanup();
+ Utils::Log(LOG_INFO, get_class($this)." end of data collection.");
+ } else {
+ Utils::Log(LOG_ERR, get_class($this)."::Prepare() returned false");
+ }
+ } catch (Exception $e) {
+ $bResult = false;
+ Utils::Log(LOG_ERR, get_class($this)."::Collect() got an exception: ".$e->getMessage());
+ }
+
+ return $bResult;
+ }
+
+ protected function AddHeader($aHeaders)
+ {
+ $this->aCSVHeaders = [];
+ foreach ($aHeaders as $sHeader) {
+ if (($sHeader != 'primary_key') && !$this->HeaderIsAllowed($sHeader)) {
+ if (!$this->AttributeIsOptional($sHeader)) {
+ Utils::Log(LOG_WARNING, "Invalid column '$sHeader', will be ignored.");
+ }
+ } else {
+ $this->aCSVHeaders[] = $sHeader;
+ }
+ }
+ fputcsv($this->aCSVFile[$this->iFileIndex], $this->aCSVHeaders, $this->sSeparator);
+ }
+
+ /**
+ * Added to add multi-column field support or situations
+ * where column name is different than attribute code.
+ *
+ * @param string $sHeader
+ * @return bool
+ */
+ protected function HeaderIsAllowed($sHeader)
+ {
+ foreach ($this->aFields as $aField) {
+ if (in_array($sHeader, $aField['columns'])) {
+ return true;
+ }
+ }
+
+ // fallback old behaviour
+ return array_key_exists($sHeader, $this->aFields);
+ }
+
+ protected function AddRow($aRow)
+ {
+ $aData = [];
+ foreach ($this->aCSVHeaders as $sHeader) {
+ if (is_null($aRow[$sHeader]) && $this->AttributeIsNullified($sHeader)) {
+ $aData[] = NULL_VALUE;
+ } else {
+ $aData[] = $aRow[$sHeader];
+ }
+ }
+ //fwrite($this->aCSVFile[$this->iFileIndex], implode($this->sSeparator, $aData)."\n");
+ fputcsv($this->aCSVFile[$this->iFileIndex], $aData, $this->sSeparator);
+ }
+
+ /**
+ * @return true
+ * @throws IOException When file cannot be opened.
+ */
+ protected function OpenCSVFile()
+ {
+ if ($this->MustProcessBeforeSynchro()) {
+ $sDataFile = Utils::GetDataFilePath(get_class($this).'.raw-'.(1 + $this->iFileIndex).'.csv');
+ } else {
+ $sDataFile = Utils::GetDataFilePath(get_class($this).'-'.(1 + $this->iFileIndex).'.csv');
+ }
+ $this->aCSVFile[$this->iFileIndex] = @fopen($sDataFile, 'wb');
+
+ if ($this->aCSVFile[$this->iFileIndex] === false) {
+ throw new IOException("Unable to open the file '$sDataFile' for writing.");
+ } else {
+ Utils::Log(LOG_INFO, "Writing to file '$sDataFile'.");
+ }
+
+ return true;
+ }
+
+ /**
+ * @return true
+ * @throws IOException When file cannot be opened.
+ */
+ protected function NextCSVFile()
+ {
+ if ($this->iFileIndex !== null) {
+ fclose($this->aCSVFile[$this->iFileIndex]);
+ $this->aCSVFile[$this->iFileIndex] = false;
+ $this->iFileIndex++;
+ } else {
+ $this->iFileIndex = 0;
+ }
+
+ return $this->OpenCSVFile();
+ }
+
+ protected function RemoveDataFiles()
+ {
+ $aFiles = glob(Utils::GetDataFilePath(get_class($this).'-*.csv'));
+ foreach ($aFiles as $sFile) {
+ $bResult = @unlink($sFile);
+ Utils::Log(LOG_DEBUG, "Erasing previous data file. unlink('$sFile') returned ".($bResult ? 'true' : 'false'));
+ }
+ $aFiles = glob(Utils::GetDataFilePath(get_class($this).'.raw-*.csv'));
+ foreach ($aFiles as $sFile) {
+ $bResult = @unlink($sFile);
+ Utils::Log(LOG_DEBUG, "Erasing previous data file. unlink('$sFile') returned ".($bResult ? 'true' : 'false'));
+ }
+ }
+
+ public function Synchronize($iMaxChunkSize = 0)
+ {
+ // Let a chance to the collector to alter/reprocess the CSV file
+ // before running the synchro. This is useful for performing advanced lookups
+ // in iTop, for example for some Typology with several cascading levels that
+ // the data synchronization can not handle directly
+ if ($this->MustProcessBeforeSynchro()) {
+ $this->DoProcessBeforeSynchro();
+ }
+
+ $aFiles = glob(Utils::GetDataFilePath(get_class($this).'-*.csv'));
+
+ //use synchro detailed output for log level 7
+ //explanation: there is a weard behaviour with LOG level under windows (some PHP versions??)
+ // under linux LOG_DEBUG=7 and LOG_INFO=6
+ // under windows LOG_DEBUG=LOG_INFO=6...
+ $bDetailedOutput = ("7" === "" . Utils::$iConsoleLogLevel);
+ foreach ($aFiles as $sDataFile) {
+ Utils::Log(LOG_INFO, "Uploading data file '$sDataFile'");
+ // Load by chunk
+
+ if ($bDetailedOutput) {
+ $sOutput = 'details';
+ } else {
+ $sOutput = 'retcode';
+ }
+
+ $aData = [
+ 'separator' => ';',
+ 'data_source_id' => $this->iSourceId,
+ 'synchronize' => '0',
+ 'no_stop_on_import_error' => 1,
+ 'output' => $sOutput,
+ 'csvdata' => file_get_contents($sDataFile),
+ 'charset' => $this->GetCharset(),
'date_format' => Utils::GetConfigurationValue('date_format', 'Y-m-d H:i:s')
- );
-
- $sLoginform = Utils::GetLoginMode();
- $sResult = self::CallItopViaHttp("/synchro/synchro_import.php?login_mode=$sLoginform",
- $aData);
-
- $sTrimmedOutput = trim(strip_tags($sResult));
- $sErrorCount = self::ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput);
-
- if ($sErrorCount != '0') {
- // hmm something went wrong
- Utils::Log(LOG_ERR, "Failed to import the data from '$sDataFile' into iTop. $sErrorCount line(s) had errors.");
- Utils::Log(LOG_ERR, $sTrimmedOutput);
-
- return false;
- } else {
- Utils::Log(LOG_DEBUG, $sTrimmedOutput);
- }
- }
-
- // Synchronize... also by chunks...
- Utils::Log(LOG_INFO, "Starting synchronization of the data source '{$this->sSourceName}'...");
- $aData = array(
- 'data_sources' => $this->iSourceId,
- );
- if ($iMaxChunkSize > 0) {
- $aData['max_chunk_size'] = $iMaxChunkSize;
- }
-
- $sLoginform = Utils::GetLoginMode();
- $sResult = self::CallItopViaHttp("/synchro/synchro_exec.php?login_mode=$sLoginform",
- $aData);
-
- $iErrorsCount = $this->ParseSynchroExecOutput($sResult);
-
- return ($iErrorsCount == 0);
- }
-
- /**
- * Detects synchro exec errors and finds out details message
- * @param string $sResult synchro_exec.php output
- * @return int error count
- * @throws Exception
- * @since 1.3.1 N°6771
- */
- public function ParseSynchroExecOutput($sResult) : int
- {
- if (preg_match_all('|sErrorMessage .= "Failed to login to iTop. Invalid (or insufficent) credentials.\n";
- return 1;
- }
-
- $iErrorsCount = 0;
- if (preg_match_all('/Objects (.*) errors: ([0-9]+)/', $sResult, $aMatches)) {
- foreach ($aMatches[2] as $sDetailedMessage => $sErrCount) {
- $iErrorsCount += (int)$sErrCount;
- if ((int)$sErrCount > 0) {
- Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' answered: {$aMatches[0][$sDetailedMessage]}");
- $this->sErrorMessage .= $aMatches[0][$sDetailedMessage]."\n";
- }
- }
-
- if (preg_match('/
ERROR: (.*)\./', $sResult, $aMatches)) {
- $sDetailedMessage = $aMatches[1];
- Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' answered: $sDetailedMessage");
- $this->sErrorMessage .= $sDetailedMessage."\n";
- $iErrorsCount++;
- }
- } else {
- Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' failed.");
- Utils::Log(LOG_DEBUG, $sResult);
- $this->sErrorMessage .= $sResult;
- return 1;
- }
-
- if ($iErrorsCount == 0) {
- Utils::Log(LOG_INFO, "Synchronization of data source '{$this->sSourceName}' succeeded.");
- }
-
- return $iErrorsCount;
- }
-
- /**
- * Detects synchro import errors and finds out details message
- * @param string $sTrimmedOutput
- * @param bool $bDetailedOutput
- *
- * @return string: error count string. should match "0" when successfull synchro import.
- * @since 1.3.1 N°6771
- */
- public static function ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput) : string
- {
- if ($bDetailedOutput)
- {
- if (preg_match('/#Issues \(before synchro\): (\d+)/', $sTrimmedOutput, $aMatches)){
- if (sizeof($aMatches)>0){
- return $aMatches[1];
- }
- }
- return -1;
- }
-
- // Read the status code from the last line
- $aLines = explode("\n", $sTrimmedOutput);
- return array_pop($aLines);
- }
-
- /**
- * @deprecated 1.3.1 N°6771
- * @see static::ParseSynchroImportOutput
- */
- public static function ParseSynchroOutput($sTrimmedOutput, $bDetailedOutput) : string {
- // log a deprecation message
- Utils::Log(LOG_INFO, "Called to deprecated method ParseSynchroOutput. Use ParseSynchroImportOutput instead.");
- return static::ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput);
- }
-
- public static function CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut = -1)
- {
- if (null === static::$oCallItopService){
- static::$oCallItopService = new CallItopService();
- }
- return static::$oCallItopService->CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut);
- }
-
- protected function CreateSynchroDataSource($aSourceDefinition, $sComment)
- {
- $oClient = new RestClient();
- $ret = false;
-
- // Ignore read-only fields
- unset($aSourceDefinition['friendlyname']);
- unset($aSourceDefinition['user_id_friendlyname']);
- unset($aSourceDefinition['user_id_finalclass_recall']);
- unset($aSourceDefinition['notify_contact_id_friendlyname']);
- unset($aSourceDefinition['notify_contact_id_finalclass_recall']);
- unset($aSourceDefinition['notify_contact_id_obsolescence_flag']);
- // SynchroAttributes will be processed one by one, below
- $aSynchroAttr = $aSourceDefinition['attribute_list'];
- unset($aSourceDefinition['attribute_list']);
-
- $aResult = $oClient->Create('SynchroDataSource', $aSourceDefinition, $sComment);
- if ($aResult['code'] == 0) {
- $aCreatedObj = reset($aResult['objects']);
- $aExpectedAttrDef = $aCreatedObj['fields']['attribute_list'];
- $iKey = (int)$aCreatedObj['key'];
- $this->iSourceId = $iKey;
-
- if ($this->UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttr, $sComment)) {
- $ret = $this->iSourceId;
- }
- } else {
- Utils::Log(LOG_ERR, "Failed to create the SynchroDataSource '{$aSourceDefinition['name']}'. Reason: {$aResult['message']} ({$aResult['code']})");
- }
-
- return $ret;
- }
-
- protected function UpdateSynchroDataSource($aSourceDefinition, $sComment)
- {
- $bRet = true;
- $oClient = new RestClient();
-
- // Ignore read-only fields
- unset($aSourceDefinition['friendlyname']);
- unset($aSourceDefinition['user_id_friendlyname']);
- unset($aSourceDefinition['user_id_finalclass_recall']);
- unset($aSourceDefinition['notify_contact_id_friendlyname']);
- unset($aSourceDefinition['notify_contact_id_finalclass_recall']);
- unset($aSourceDefinition['notify_contact_id_obsolescence_flag']);
- // SynchroAttributes will be processed one by one, below
- $aSynchroAttr = $aSourceDefinition['attribute_list'];
- unset($aSourceDefinition['attribute_list']);
-
- $aResult = $oClient->Update('SynchroDataSource', $this->iSourceId, $aSourceDefinition, $sComment);
- $bRet = ($aResult['code'] == 0);
- if ($bRet) {
- $aUpdatedObj = reset($aResult['objects']);
- $aExpectedAttrDef = $aUpdatedObj['fields']['attribute_list'];
- $bRet = $this->UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttr, $sComment);
- } else {
- Utils::Log(LOG_ERR, "Failed to update the SynchroDataSource '{$aSourceDefinition['name']}' ({$this->iSourceId}). Reason: {$aResult['message']} ({$aResult['code']})");
- }
-
- return $bRet;
- }
-
- protected function UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttrDef, $sComment, ?RestClient $oClient = null)
- {
- $bRet = true;
- if ($oClient === null)
- {
- $oClient = new RestClient();
- }
-
- foreach ($aSynchroAttrDef as $aAttr) {
- $aExpectedAttr = $this->FindAttr($aAttr['attcode'], $aExpectedAttrDef);
-
- if ($aAttr != $aExpectedAttr) {
+ ];
+
+ $sLoginform = Utils::GetLoginMode();
+ $sResult = self::CallItopViaHttp(
+ "/synchro/synchro_import.php?login_mode=$sLoginform",
+ $aData
+ );
+
+ $sTrimmedOutput = trim(strip_tags($sResult));
+ $sErrorCount = self::ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput);
+
+ if ($sErrorCount != '0') {
+ // hmm something went wrong
+ Utils::Log(LOG_ERR, "Failed to import the data from '$sDataFile' into iTop. $sErrorCount line(s) had errors.");
+ Utils::Log(LOG_ERR, $sTrimmedOutput);
+
+ return false;
+ } else {
+ Utils::Log(LOG_DEBUG, $sTrimmedOutput);
+ }
+ }
+
+ // Synchronize... also by chunks...
+ Utils::Log(LOG_INFO, "Starting synchronization of the data source '{$this->sSourceName}'...");
+ $aData = [
+ 'data_sources' => $this->iSourceId,
+ ];
+ if ($iMaxChunkSize > 0) {
+ $aData['max_chunk_size'] = $iMaxChunkSize;
+ }
+
+ $sLoginform = Utils::GetLoginMode();
+ $sResult = self::CallItopViaHttp(
+ "/synchro/synchro_exec.php?login_mode=$sLoginform",
+ $aData
+ );
+
+ $iErrorsCount = $this->ParseSynchroExecOutput($sResult);
+
+ return ($iErrorsCount == 0);
+ }
+
+ /**
+ * Detects synchro exec errors and finds out details message
+ * @param string $sResult synchro_exec.php output
+ * @return int error count
+ * @throws Exception
+ * @since 1.3.1 N°6771
+ */
+ public function ParseSynchroExecOutput($sResult): int
+ {
+ if (preg_match_all('|sErrorMessage .= "Failed to login to iTop. Invalid (or insufficent) credentials.\n";
+ return 1;
+ }
+
+ $iErrorsCount = 0;
+ if (preg_match_all('/Objects (.*) errors: ([0-9]+)/', $sResult, $aMatches)) {
+ foreach ($aMatches[2] as $sDetailedMessage => $sErrCount) {
+ $iErrorsCount += (int)$sErrCount;
+ if ((int)$sErrCount > 0) {
+ Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' answered: {$aMatches[0][$sDetailedMessage]}");
+ $this->sErrorMessage .= $aMatches[0][$sDetailedMessage]."\n";
+ }
+ }
+
+ if (preg_match('/
ERROR: (.*)\./', $sResult, $aMatches)) {
+ $sDetailedMessage = $aMatches[1];
+ Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' answered: $sDetailedMessage");
+ $this->sErrorMessage .= $sDetailedMessage."\n";
+ $iErrorsCount++;
+ }
+ } else {
+ Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' failed.");
+ Utils::Log(LOG_DEBUG, $sResult);
+ $this->sErrorMessage .= $sResult;
+ return 1;
+ }
+
+ if ($iErrorsCount == 0) {
+ Utils::Log(LOG_INFO, "Synchronization of data source '{$this->sSourceName}' succeeded.");
+ }
+
+ return $iErrorsCount;
+ }
+
+ /**
+ * Detects synchro import errors and finds out details message
+ * @param string $sTrimmedOutput
+ * @param bool $bDetailedOutput
+ *
+ * @return string: error count string. should match "0" when successfull synchro import.
+ * @since 1.3.1 N°6771
+ */
+ public static function ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput): string
+ {
+ if ($bDetailedOutput) {
+ if (preg_match('/#Issues \(before synchro\): (\d+)/', $sTrimmedOutput, $aMatches)) {
+ if (sizeof($aMatches) > 0) {
+ return $aMatches[1];
+ }
+ }
+ return -1;
+ }
+
+ // Read the status code from the last line
+ $aLines = explode("\n", $sTrimmedOutput);
+ return array_pop($aLines);
+ }
+
+ /**
+ * @deprecated 1.3.1 N°6771
+ * @see static::ParseSynchroImportOutput
+ */
+ public static function ParseSynchroOutput($sTrimmedOutput, $bDetailedOutput): string
+ {
+ // log a deprecation message
+ Utils::Log(LOG_INFO, "Called to deprecated method ParseSynchroOutput. Use ParseSynchroImportOutput instead.");
+ return static::ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput);
+ }
+
+ public static function CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut = -1)
+ {
+ if (null === static::$oCallItopService) {
+ static::$oCallItopService = new CallItopService();
+ }
+ return static::$oCallItopService->CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut);
+ }
+
+ protected function CreateSynchroDataSource($aSourceDefinition, $sComment)
+ {
+ $oClient = new RestClient();
+ $ret = false;
+
+ // Ignore read-only fields
+ unset($aSourceDefinition['friendlyname']);
+ unset($aSourceDefinition['user_id_friendlyname']);
+ unset($aSourceDefinition['user_id_finalclass_recall']);
+ unset($aSourceDefinition['notify_contact_id_friendlyname']);
+ unset($aSourceDefinition['notify_contact_id_finalclass_recall']);
+ unset($aSourceDefinition['notify_contact_id_obsolescence_flag']);
+ // SynchroAttributes will be processed one by one, below
+ $aSynchroAttr = $aSourceDefinition['attribute_list'];
+ unset($aSourceDefinition['attribute_list']);
+
+ $aResult = $oClient->Create('SynchroDataSource', $aSourceDefinition, $sComment);
+ if ($aResult['code'] == 0) {
+ $aCreatedObj = reset($aResult['objects']);
+ $aExpectedAttrDef = $aCreatedObj['fields']['attribute_list'];
+ $iKey = (int)$aCreatedObj['key'];
+ $this->iSourceId = $iKey;
+
+ if ($this->UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttr, $sComment)) {
+ $ret = $this->iSourceId;
+ }
+ } else {
+ Utils::Log(LOG_ERR, "Failed to create the SynchroDataSource '{$aSourceDefinition['name']}'. Reason: {$aResult['message']} ({$aResult['code']})");
+ }
+
+ return $ret;
+ }
+
+ protected function UpdateSynchroDataSource($aSourceDefinition, $sComment)
+ {
+ $bRet = true;
+ $oClient = new RestClient();
+
+ // Ignore read-only fields
+ unset($aSourceDefinition['friendlyname']);
+ unset($aSourceDefinition['user_id_friendlyname']);
+ unset($aSourceDefinition['user_id_finalclass_recall']);
+ unset($aSourceDefinition['notify_contact_id_friendlyname']);
+ unset($aSourceDefinition['notify_contact_id_finalclass_recall']);
+ unset($aSourceDefinition['notify_contact_id_obsolescence_flag']);
+ // SynchroAttributes will be processed one by one, below
+ $aSynchroAttr = $aSourceDefinition['attribute_list'];
+ unset($aSourceDefinition['attribute_list']);
+
+ $aResult = $oClient->Update('SynchroDataSource', $this->iSourceId, $aSourceDefinition, $sComment);
+ $bRet = ($aResult['code'] == 0);
+ if ($bRet) {
+ $aUpdatedObj = reset($aResult['objects']);
+ $aExpectedAttrDef = $aUpdatedObj['fields']['attribute_list'];
+ $bRet = $this->UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttr, $sComment);
+ } else {
+ Utils::Log(LOG_ERR, "Failed to update the SynchroDataSource '{$aSourceDefinition['name']}' ({$this->iSourceId}). Reason: {$aResult['message']} ({$aResult['code']})");
+ }
+
+ return $bRet;
+ }
+
+ protected function UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttrDef, $sComment, ?RestClient $oClient = null)
+ {
+ $bRet = true;
+ if ($oClient === null) {
+ $oClient = new RestClient();
+ }
+
+ foreach ($aSynchroAttrDef as $aAttr) {
+ $aExpectedAttr = $this->FindAttr($aAttr['attcode'], $aExpectedAttrDef);
+
+ if ($aAttr != $aExpectedAttr) {
if (($aExpectedAttr === false) && $this->AttributeIsOptional($aAttr['attcode'])) {
- Utils::Log(LOG_INFO, "Skipping optional (and not-present) attribute '{$aAttr['attcode']}'.");
- $this->aSkippedAttributes[] = $aAttr['attcode']; // record that this attribute was skipped
-
- } else {
- // Update only the SynchroAttributes which are really different
- // Ignore read-only fields
- unset($aAttr['friendlyname']);
- $sTargetClass = $aAttr['finalclass'];
- unset($aAttr['finalclass']);
- unset($aAttr['import_columns']);
- // Fix booleans
- $aAttr['update'] = ($aAttr['update'] == 1) ? "1" : "0";
- $aAttr['reconcile'] = ($aAttr['reconcile'] == 1) ? "1" : "0";
-
- Utils::Log(LOG_DEBUG, "Updating attribute '{$aAttr['attcode']}'.");
- $aResult = $oClient->Update($sTargetClass, array('sync_source_id' => $this->iSourceId, 'attcode' => $aAttr['attcode']), $aAttr, $sComment);
- $bRet = ($aResult['code'] == 0);
- if (!$bRet) {
- if (preg_match('/Error: No item found with criteria: sync_source_id/', $aResult['message'])) {
- Utils::Log(LOG_ERR, "Failed to update the Synchro Data Source. Inconsistent data model, the attribute '{$aAttr['attcode']}' does not exist in iTop.");
- } else {
- Utils::Log(LOG_ERR, "Failed to update the SynchroAttribute '{$aAttr['attcode']}'. Reason: {$aResult['message']} ({$aResult['code']})");
- }
- break;
- }
- }
- }
- }
-
- return $bRet;
- }
-
- /**
- * Find the definition of the specified attribute in 'attribute_list'
- *
- * @param string $sAttCode
- * @param array $aExpectedAttrDef
- *
- * @return array|boolean
- */
- protected function FindAttr($sAttCode, $aExpectedAttrDef)
- {
- foreach ($aExpectedAttrDef as $aAttr) {
- if ($aAttr['attcode'] == $sAttCode) {
- return $aAttr;
- }
- }
-
- return false;
- }
-
- /**
- * Smart comparison of two data sources definitions, ignoring optional attributes
- *
- * @param array $aDS1
- * @param array $aDS2
- */
- protected function DataSourcesAreEquivalent($aDS1, $aDS2)
- {
- foreach ($aDS1 as $sKey => $value) {
- switch ($sKey) {
- case 'friendlyname':
- case 'user_id_friendlyname':
- case 'user_id_finalclass_recall':
- case 'notify_contact_id_friendlyname':
- case 'notify_contact_id_finalclass_recall':
- case 'notify_contact_id_obsolescence_flag':
- case 'notify_contact_id_archive_flag':
- // Ignore all read-only attributes
- break;
-
- case 'attribute_list':
- foreach ($value as $sKey => $aDef) {
- $sAttCode = $aDef['attcode'];
- $aDef2 = $this->FindAttr($sAttCode, $aDS2['attribute_list']);
- if ($aDef2 === false) {
- if ($this->AttributeIsOptional($sAttCode)) {
- // Ignore missing optional attributes
- Utils::Log(LOG_DEBUG, "Comparison: ignoring the missing, but optional, attribute: '$sAttCode'.");
- $this->aSkippedAttributes[] = $sAttCode;
- continue;
- } else {
- // Missing non-optional attribute
- Utils::Log(LOG_DEBUG, "Comparison: The definition of the non-optional attribute '$sAttCode' is missing. Data sources differ.");
-
- return false;
- }
-
- } else {
- if (($aDef != $aDef2) && (!$this->AttributeIsOptional($sAttCode))) {
- // Definitions are different
- Utils::Log(LOG_DEBUG, "Comparison: The definitions of the attribute '$sAttCode' are different. Data sources differ:\nExpected values:".print_r($aDef, true)."------------\nCurrent values in iTop:".print_r($aDef2, true)."\n");
-
- return false;
- }
- }
- }
-
- // Now check the other way around: are there too many attributes defined?
- foreach ($aDS2['attribute_list'] as $sKey => $aDef) {
- $sAttCode = $aDef['attcode'];
- if (!$this->FindAttr($sAttCode, $aDS1['attribute_list']) && !$this->AttributeIsOptional($sAttCode)) {
- Utils::Log(LOG_NOTICE, "Comparison: Found the extra definition of the non-optional attribute '$sAttCode' in iTop. Data sources differ. Nothing to do. Update json definition if you want to update this field in iTop.");
- }
- }
- break;
-
- default:
- if (!array_key_exists($sKey, $aDS2) || $aDS2[$sKey] != $value) {
- if ($sKey != 'database_table_name') {
- // one meaningful difference is enough
- Utils::Log(LOG_DEBUG, "Comparison: The property '$sKey' is missing or has a different value. Data sources differ.");
-
- return false;
- }
- }
- }
- }
- //Check the other way around
- foreach ($aDS2 as $sKey => $value) {
- switch ($sKey) {
- case 'friendlyname':
- case 'user_id_friendlyname':
- case 'user_id_finalclass_recall':
- case 'notify_contact_id_friendlyname':
- case 'notify_contact_id_finalclass_recall':
- case 'notify_contact_id_obsolescence_flag':
- case 'notify_contact_id_archive_flag':
- // Ignore all read-only attributes
- break;
-
- default:
- if (!array_key_exists($sKey, $aDS1)) {
- Utils::Log(LOG_DEBUG, "Comparison: Found an extra property '$sKey' in iTop. Data sources differ.");
-
- return false;
- }
- }
- }
-
- return true;
- }
-
- /**
- * @param string $sStep
- *
- * @return array
- */
- public function GetErrorStatus($sStep)
- {
- return [
- 'status' => false,
- 'exit_status' => false,
- 'project' => $this->GetProjectName(),
- 'collector' => get_class($this),
- 'message' => '',
- 'step' => $sStep,
- ];
- }
-
- /**
- * Check if the keys of the supplied hash array match the expected fields listed in the data synchro
- *
- * @param array $aSynchroColumns attribute name as key, attribute's value as value (value not used)
- * @param $aColumnsToIgnore : Elements to ignore
- * @param $sSource : Source of the request (Json file, SQL query, csv file...)
- *
- * @throws \Exception
- */
- protected function CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource)
- {
- $sClass = get_class($this);
- $iError = 0;
-
- if (!array_key_exists('primary_key', $aSynchroColumns)) {
- Utils::Log(LOG_ERR, '['.$sClass.'] The mandatory column "primary_key" is missing in the '.$sSource.'.');
- $iError++;
- }
- foreach ($this->aFields as $sCode => $aDefs) {
- // Skip attributes to ignore
- if (in_array($sCode, $aColumnsToIgnore)) {
- continue;
- }
- // Skip optional attributes
- if ($this->AttributeIsOptional($sCode)) {
- continue;
- }
-
- // Check for missing columns
- $aMissingColumns = array_diff($aDefs['columns'], array_keys($aSynchroColumns));
- if (!empty($aMissingColumns) && $aDefs['reconcile']) {
- Utils::Log(LOG_ERR, sprintf('[%s] The field "%s", used for reconciliation, has missing column(s) in the %s.', $sClass, $sCode, $sSource));
- $iError++;
- Utils::Log(LOG_DEBUG, sprintf('[%s] Missing columns: %s', $sClass, implode(', ', $aMissingColumns)));
- } elseif (!empty($aMissingColumns) && $aDefs['update']) {
- if ($this->AttributeIsNullified($sCode)){
- Utils::Log(LOG_DEBUG, '['.$sClass.'] The field "'.$sCode.'", used for update, has missing column(s) in first row but nullified.');
- continue;
- }
- Utils::Log(LOG_ERR, sprintf('[%s] The field "%s", used for update, has missing column(s) in the %s.', $sClass, $sCode, $sSource));
- $iError++;
- Utils::Log(LOG_DEBUG, sprintf('[%s] Missing columns: %s', $sClass, implode(', ', $aMissingColumns)));
- }
-
- // Check for useless columns
- if (empty($aMissingColumns) && !$aDefs['reconcile'] && !$aDefs['update']) {
- Utils::Log(LOG_WARNING, sprintf('[%s] The field "%s" is used neither for update nor for reconciliation.', $sClass, $sCode));
- }
-
- }
-
- if ($iError > 0) {
- throw new Exception("Missing columns in the ".$sSource.'.');
- }
- }
+ Utils::Log(LOG_INFO, "Skipping optional (and not-present) attribute '{$aAttr['attcode']}'.");
+ $this->aSkippedAttributes[] = $aAttr['attcode']; // record that this attribute was skipped
+
+ } else {
+ // Update only the SynchroAttributes which are really different
+ // Ignore read-only fields
+ unset($aAttr['friendlyname']);
+ $sTargetClass = $aAttr['finalclass'];
+ unset($aAttr['finalclass']);
+ unset($aAttr['import_columns']);
+ // Fix booleans
+ $aAttr['update'] = ($aAttr['update'] == 1) ? "1" : "0";
+ $aAttr['reconcile'] = ($aAttr['reconcile'] == 1) ? "1" : "0";
+
+ Utils::Log(LOG_DEBUG, "Updating attribute '{$aAttr['attcode']}'.");
+ $aResult = $oClient->Update($sTargetClass, ['sync_source_id' => $this->iSourceId, 'attcode' => $aAttr['attcode']], $aAttr, $sComment);
+ $bRet = ($aResult['code'] == 0);
+ if (!$bRet) {
+ if (preg_match('/Error: No item found with criteria: sync_source_id/', $aResult['message'])) {
+ Utils::Log(LOG_ERR, "Failed to update the Synchro Data Source. Inconsistent data model, the attribute '{$aAttr['attcode']}' does not exist in iTop.");
+ } else {
+ Utils::Log(LOG_ERR, "Failed to update the SynchroAttribute '{$aAttr['attcode']}'. Reason: {$aResult['message']} ({$aResult['code']})");
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ return $bRet;
+ }
+
+ /**
+ * Find the definition of the specified attribute in 'attribute_list'
+ *
+ * @param string $sAttCode
+ * @param array $aExpectedAttrDef
+ *
+ * @return array|boolean
+ */
+ protected function FindAttr($sAttCode, $aExpectedAttrDef)
+ {
+ foreach ($aExpectedAttrDef as $aAttr) {
+ if ($aAttr['attcode'] == $sAttCode) {
+ return $aAttr;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Smart comparison of two data sources definitions, ignoring optional attributes
+ *
+ * @param array $aDS1
+ * @param array $aDS2
+ */
+ protected function DataSourcesAreEquivalent($aDS1, $aDS2)
+ {
+ foreach ($aDS1 as $sKey => $value) {
+ switch ($sKey) {
+ case 'friendlyname':
+ case 'user_id_friendlyname':
+ case 'user_id_finalclass_recall':
+ case 'notify_contact_id_friendlyname':
+ case 'notify_contact_id_finalclass_recall':
+ case 'notify_contact_id_obsolescence_flag':
+ case 'notify_contact_id_archive_flag':
+ // Ignore all read-only attributes
+ break;
+
+ case 'attribute_list':
+ foreach ($value as $sKey => $aDef) {
+ $sAttCode = $aDef['attcode'];
+ $aDef2 = $this->FindAttr($sAttCode, $aDS2['attribute_list']);
+ if ($aDef2 === false) {
+ if ($this->AttributeIsOptional($sAttCode)) {
+ // Ignore missing optional attributes
+ Utils::Log(LOG_DEBUG, "Comparison: ignoring the missing, but optional, attribute: '$sAttCode'.");
+ $this->aSkippedAttributes[] = $sAttCode;
+ continue;
+ } else {
+ // Missing non-optional attribute
+ Utils::Log(LOG_DEBUG, "Comparison: The definition of the non-optional attribute '$sAttCode' is missing. Data sources differ.");
+
+ return false;
+ }
+
+ } else {
+ if (($aDef != $aDef2) && (!$this->AttributeIsOptional($sAttCode))) {
+ // Definitions are different
+ Utils::Log(LOG_DEBUG, "Comparison: The definitions of the attribute '$sAttCode' are different. Data sources differ:\nExpected values:".print_r($aDef, true)."------------\nCurrent values in iTop:".print_r($aDef2, true)."\n");
+
+ return false;
+ }
+ }
+ }
+
+ // Now check the other way around: are there too many attributes defined?
+ foreach ($aDS2['attribute_list'] as $sKey => $aDef) {
+ $sAttCode = $aDef['attcode'];
+ if (!$this->FindAttr($sAttCode, $aDS1['attribute_list']) && !$this->AttributeIsOptional($sAttCode)) {
+ Utils::Log(LOG_NOTICE, "Comparison: Found the extra definition of the non-optional attribute '$sAttCode' in iTop. Data sources differ. Nothing to do. Update json definition if you want to update this field in iTop.");
+ }
+ }
+ break;
+
+ default:
+ if (!array_key_exists($sKey, $aDS2) || $aDS2[$sKey] != $value) {
+ if ($sKey != 'database_table_name') {
+ // one meaningful difference is enough
+ Utils::Log(LOG_DEBUG, "Comparison: The property '$sKey' is missing or has a different value. Data sources differ.");
+
+ return false;
+ }
+ }
+ }
+ }
+ //Check the other way around
+ foreach ($aDS2 as $sKey => $value) {
+ switch ($sKey) {
+ case 'friendlyname':
+ case 'user_id_friendlyname':
+ case 'user_id_finalclass_recall':
+ case 'notify_contact_id_friendlyname':
+ case 'notify_contact_id_finalclass_recall':
+ case 'notify_contact_id_obsolescence_flag':
+ case 'notify_contact_id_archive_flag':
+ // Ignore all read-only attributes
+ break;
+
+ default:
+ if (!array_key_exists($sKey, $aDS1)) {
+ Utils::Log(LOG_DEBUG, "Comparison: Found an extra property '$sKey' in iTop. Data sources differ.");
+
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $sStep
+ *
+ * @return array
+ */
+ public function GetErrorStatus($sStep)
+ {
+ return [
+ 'status' => false,
+ 'exit_status' => false,
+ 'project' => $this->GetProjectName(),
+ 'collector' => get_class($this),
+ 'message' => '',
+ 'step' => $sStep,
+ ];
+ }
+
+ /**
+ * Check if the keys of the supplied hash array match the expected fields listed in the data synchro
+ *
+ * @param array $aSynchroColumns attribute name as key, attribute's value as value (value not used)
+ * @param $aColumnsToIgnore : Elements to ignore
+ * @param $sSource : Source of the request (Json file, SQL query, csv file...)
+ *
+ * @throws \Exception
+ */
+ protected function CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource)
+ {
+ $sClass = get_class($this);
+ $iError = 0;
+
+ if (!array_key_exists('primary_key', $aSynchroColumns)) {
+ Utils::Log(LOG_ERR, '['.$sClass.'] The mandatory column "primary_key" is missing in the '.$sSource.'.');
+ $iError++;
+ }
+ foreach ($this->aFields as $sCode => $aDefs) {
+ // Skip attributes to ignore
+ if (in_array($sCode, $aColumnsToIgnore)) {
+ continue;
+ }
+ // Skip optional attributes
+ if ($this->AttributeIsOptional($sCode)) {
+ continue;
+ }
+
+ // Check for missing columns
+ $aMissingColumns = array_diff($aDefs['columns'], array_keys($aSynchroColumns));
+ if (!empty($aMissingColumns) && $aDefs['reconcile']) {
+ Utils::Log(LOG_ERR, sprintf('[%s] The field "%s", used for reconciliation, has missing column(s) in the %s.', $sClass, $sCode, $sSource));
+ $iError++;
+ Utils::Log(LOG_DEBUG, sprintf('[%s] Missing columns: %s', $sClass, implode(', ', $aMissingColumns)));
+ } elseif (!empty($aMissingColumns) && $aDefs['update']) {
+ if ($this->AttributeIsNullified($sCode)) {
+ Utils::Log(LOG_DEBUG, '['.$sClass.'] The field "'.$sCode.'", used for update, has missing column(s) in first row but nullified.');
+ continue;
+ }
+ Utils::Log(LOG_ERR, sprintf('[%s] The field "%s", used for update, has missing column(s) in the %s.', $sClass, $sCode, $sSource));
+ $iError++;
+ Utils::Log(LOG_DEBUG, sprintf('[%s] Missing columns: %s', $sClass, implode(', ', $aMissingColumns)));
+ }
+
+ // Check for useless columns
+ if (empty($aMissingColumns) && !$aDefs['reconcile'] && !$aDefs['update']) {
+ Utils::Log(LOG_WARNING, sprintf('[%s] The field "%s" is used neither for update nor for reconciliation.', $sClass, $sCode));
+ }
+
+ }
+
+ if ($iError > 0) {
+ throw new Exception("Missing columns in the ".$sSource.'.');
+ }
+ }
/*
- * Check if the collector can be launched
- *
+ * Check if the collector can be launched
+ *
* @param $aOrchestratedCollectors = list of collectors already orchestrated
*
- * @return bool
- */
- public function CheckToLaunch(array $aOrchestratedCollectors): bool {
- return true;
- }
+ * @return bool
+ */
+ public function CheckToLaunch(array $aOrchestratedCollectors): bool
+ {
+ return true;
+ }
}
diff --git a/core/csvcollector.class.inc.php b/core/csvcollector.class.inc.php
index fb05974..b9ab208 100644
--- a/core/csvcollector.class.inc.php
+++ b/core/csvcollector.class.inc.php
@@ -26,114 +26,119 @@
*/
abstract class CSVCollector extends Collector
{
- protected $iIdx = 0;
- protected $aCsvFieldsPerLine = array();
- protected $sCsvSeparator;
- protected $sCsvEncoding;
- protected $bHasHeader = true;
- protected $sCsvCliCommand;
+ protected $iIdx = 0;
+ protected $aCsvFieldsPerLine = [];
+ protected $sCsvSeparator;
+ protected $sCsvEncoding;
+ protected $bHasHeader = true;
+ protected $sCsvCliCommand;
/**
* @var array Table of number of columns in input csv file corresponding to output fields or input columns names (if the specification never mention the input column)
* [0 => [synchro_field1, synchro_field2], 1 => [synchro_field3], 2 => [col_name]]
*/
- protected $aMappingCsvColumnIndexToFields;
- protected $aSynchroFieldsToDefaultValues = array();
- protected $aConfiguredHeaderColumns;
+ protected $aMappingCsvColumnIndexToFields;
+ protected $aSynchroFieldsToDefaultValues = [];
+ protected $aConfiguredHeaderColumns;
/**
* @var array Mapping of csv columns to synchro fields
* [column_name => [synchro_field1, synchro_field2], column_name2 => [synchro_field3]]
*/
- protected $aMappingCsvColumnNameToFields = array();
+ protected $aMappingCsvColumnNameToFields = [];
/**
* @var array Table of all synchronised fields
* [synchro_field => '', synchro_field2 => '']
*/
- protected $aMappedFields = array();
- protected $aIgnoredCsvColumns = array();
- protected $aIgnoredSynchroFields = array();
-
- /**
- * Initalization
- *
- * @throws Exception
- */
- public function __construct()
- {
- parent::__construct();
- }
-
- /**
- * Parses configured csv file to fetch data
- *
- * @see Collector::Prepare()
- * @throws Exception
- */
- public function Prepare()
- {
- $bRet = parent::Prepare();
-
- $this->sCsvSeparator = ';';
- $this->sCsvEncoding = 'UTF-8';
- $this->sCsvCliCommand = '';
- $this->aSynchroFieldsToDefaultValues = array();
- $this->bHasHeader = true;
-
- $sCsvFilePath='';
- if (is_array($this->aCollectorConfig)) {
- if (array_key_exists('csv_file', $this->aCollectorConfig)) {
- $sCsvFilePath = $this->aCollectorConfig['csv_file'];
- }
-
- if (array_key_exists('separator', $this->aCollectorConfig)) {
- $this->sCsvSeparator = $this->aCollectorConfig['separator'];
- if ($this->sCsvSeparator === 'TAB') {
- $this->sCsvSeparator = "\t";
- }
- }
- if (array_key_exists('encoding', $this->aCollectorConfig)) {
- $this->sCsvEncoding = $this->aCollectorConfig['encoding'];
- }
- if (array_key_exists('command', $this->aCollectorConfig)) {
- $this->sCsvCliCommand = $this->aCollectorConfig['command'];
- }
- if (array_key_exists('has_header', $this->aCollectorConfig)) {
- $this->bHasHeader = ($this->aCollectorConfig['has_header'] !== 'no');
- }
-
-
- if (array_key_exists('defaults', $this->aCollectorConfig)) {
- if ($this->aCollectorConfig['defaults'] !== '') {
- $this->aSynchroFieldsToDefaultValues = $this->aCollectorConfig['defaults'];
- if (!is_array($this->aSynchroFieldsToDefaultValues)) {
- Utils::Log(LOG_ERR,
- "[".get_class($this)."] defaults section configuration is not correct. please see documentation.");
-
- return false;
- }
- }
- }
-
- if (array_key_exists('ignored_columns', $this->aCollectorConfig)) {
- if ($this->aCollectorConfig['ignored_columns'] !== '') {
- if (!is_array($this->aCollectorConfig['ignored_columns'])) {
- Utils::Log(LOG_ERR,
- "[".get_class($this)."] ignored_columns section configuration is not correct. please see documentation.");
-
- return false;
- }
- $this->aIgnoredCsvColumns = array_values($this->aCollectorConfig['ignored_columns']);
- }
- }
-
- if (array_key_exists('fields', $this->aCollectorConfig)) {
- if ($this->aCollectorConfig['fields'] !== '') {
- $aCurrentConfiguredHeaderColumns = $this->aCollectorConfig['fields'];
- if (!is_array($aCurrentConfiguredHeaderColumns)) {
- Utils::Log(LOG_ERR,
- "[".get_class($this)."] fields section configuration is not correct. please see documentation.");
-
- return false;
- }
+ protected $aMappedFields = [];
+ protected $aIgnoredCsvColumns = [];
+ protected $aIgnoredSynchroFields = [];
+
+ /**
+ * Initalization
+ *
+ * @throws Exception
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Parses configured csv file to fetch data
+ *
+ * @see Collector::Prepare()
+ * @throws Exception
+ */
+ public function Prepare()
+ {
+ $bRet = parent::Prepare();
+
+ $this->sCsvSeparator = ';';
+ $this->sCsvEncoding = 'UTF-8';
+ $this->sCsvCliCommand = '';
+ $this->aSynchroFieldsToDefaultValues = [];
+ $this->bHasHeader = true;
+
+ $sCsvFilePath = '';
+ if (is_array($this->aCollectorConfig)) {
+ if (array_key_exists('csv_file', $this->aCollectorConfig)) {
+ $sCsvFilePath = $this->aCollectorConfig['csv_file'];
+ }
+
+ if (array_key_exists('separator', $this->aCollectorConfig)) {
+ $this->sCsvSeparator = $this->aCollectorConfig['separator'];
+ if ($this->sCsvSeparator === 'TAB') {
+ $this->sCsvSeparator = "\t";
+ }
+ }
+ if (array_key_exists('encoding', $this->aCollectorConfig)) {
+ $this->sCsvEncoding = $this->aCollectorConfig['encoding'];
+ }
+ if (array_key_exists('command', $this->aCollectorConfig)) {
+ $this->sCsvCliCommand = $this->aCollectorConfig['command'];
+ }
+ if (array_key_exists('has_header', $this->aCollectorConfig)) {
+ $this->bHasHeader = ($this->aCollectorConfig['has_header'] !== 'no');
+ }
+
+ if (array_key_exists('defaults', $this->aCollectorConfig)) {
+ if ($this->aCollectorConfig['defaults'] !== '') {
+ $this->aSynchroFieldsToDefaultValues = $this->aCollectorConfig['defaults'];
+ if (!is_array($this->aSynchroFieldsToDefaultValues)) {
+ Utils::Log(
+ LOG_ERR,
+ "[".get_class($this)."] defaults section configuration is not correct. please see documentation."
+ );
+
+ return false;
+ }
+ }
+ }
+
+ if (array_key_exists('ignored_columns', $this->aCollectorConfig)) {
+ if ($this->aCollectorConfig['ignored_columns'] !== '') {
+ if (!is_array($this->aCollectorConfig['ignored_columns'])) {
+ Utils::Log(
+ LOG_ERR,
+ "[".get_class($this)."] ignored_columns section configuration is not correct. please see documentation."
+ );
+
+ return false;
+ }
+ $this->aIgnoredCsvColumns = array_values($this->aCollectorConfig['ignored_columns']);
+ }
+ }
+
+ if (array_key_exists('fields', $this->aCollectorConfig)) {
+ if ($this->aCollectorConfig['fields'] !== '') {
+ $aCurrentConfiguredHeaderColumns = $this->aCollectorConfig['fields'];
+ if (!is_array($aCurrentConfiguredHeaderColumns)) {
+ Utils::Log(
+ LOG_ERR,
+ "[".get_class($this)."] fields section configuration is not correct. please see documentation."
+ );
+
+ return false;
+ }
$this->aConfiguredHeaderColumns = [];
if ($this->bHasHeader) {
array_multisort($aCurrentConfiguredHeaderColumns);
@@ -150,120 +155,124 @@ public function Prepare()
}
}
- if ($sCsvFilePath === '') {
- // No query at all !!
- Utils::Log(LOG_ERR,
- "[".get_class($this)."] no CSV file configured! Cannot collect data. The csv was expected to be configured as '".strtolower(get_class($this))."_csv' in the configuration file.");
+ if ($sCsvFilePath === '') {
+ // No query at all !!
+ Utils::Log(
+ LOG_ERR,
+ "[".get_class($this)."] no CSV file configured! Cannot collect data. The csv was expected to be configured as '".strtolower(get_class($this))."_csv' in the configuration file."
+ );
- return false;
- }
+ return false;
+ }
- Utils::Log(LOG_INFO, "[".get_class($this)."] CSV file is [".$sCsvFilePath."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Has csv header [".($this->bHasHeader?"yes":"no")."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Separator used is [".$this->sCsvSeparator."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Encoding used is [".$this->sCsvEncoding."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Fields [".var_export($this->aConfiguredHeaderColumns, true)."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Ignored csv fields [".var_export($this->aIgnoredCsvColumns, true)."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Default values [".var_export($this->aSynchroFieldsToDefaultValues, true)."]");
+ Utils::Log(LOG_INFO, "[".get_class($this)."] CSV file is [".$sCsvFilePath."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Has csv header [".($this->bHasHeader ? "yes" : "no")."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Separator used is [".$this->sCsvSeparator."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Encoding used is [".$this->sCsvEncoding."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Fields [".var_export($this->aConfiguredHeaderColumns, true)."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Ignored csv fields [".var_export($this->aIgnoredCsvColumns, true)."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Default values [".var_export($this->aSynchroFieldsToDefaultValues, true)."]");
- if (!empty($this->sCsvCliCommand)) {
+ if (!empty($this->sCsvCliCommand)) {
Utils::Exec($this->sCsvCliCommand);
- }
-
- try {
- $hHandle = fopen($sCsvFilePath, "r");
- } catch (Exception $e) {
- Utils::Log(LOG_INFO, "[".get_class($this)."] Cannot open CSV file $sCsvFilePath");
- $sCsvFilePath = APPROOT.$sCsvFilePath;
- try {
- $hHandle = fopen($sCsvFilePath, "r");
- } catch (Exception $e) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] Cannot open CSV file $sCsvFilePath");
-
- return false;
- }
- }
-
- if (!$hHandle) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] Cannot use CSV file handle for $sCsvFilePath");
-
- return false;
- }
-
- $sTmpFile = tempnam(sys_get_temp_dir(), "decoded_");
- file_put_contents($sTmpFile, iconv($this->sCsvEncoding, $this->GetCharset(), stream_get_contents($hHandle)));
- $oTmpHandle = fopen($sTmpFile, "r");
-
- while (($aData = fgetcsv($oTmpHandle, 0, $this->sCsvSeparator)) !== false) {
- $this->aCsvFieldsPerLine[] = $aData;
- }
-
- fclose($oTmpHandle);
- unlink($sTmpFile);
-
- return $bRet;
- }
-
- /**
- * @return NextLineObject
- */
- public function getNextLine()
- {
- $aValues = $this->aCsvFieldsPerLine[$this->iIdx];
- $sCsvLine = implode($this->sCsvSeparator, $aValues);
-
- return new NextLineObject($sCsvLine, $aValues);
- }
-
- /**
- * Fetches one csv row at a time
- * The first row is used to check if the columns of the result match the expected "fields"
- *
- * @see Collector::Fetch()
- * @throws Exception
- */
- public function Fetch()
- {
- $iCount = count($this->aCsvFieldsPerLine);
- if (($iCount == 0) || (($iCount == 1) && $this->bHasHeader)) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] CSV file is empty. Data collection stops here.");
-
- return false;
- }
- if ($this->iIdx >= $iCount) {
-
- return false;
- }
-
- /** NextLineObject**/
- $oNextLineArr = $this->getNextLine();
-
- if (!$this->aMappingCsvColumnIndexToFields) {
- $aCsvHeaderColumns = $oNextLineArr->getValues();
-
- $this->Configure($aCsvHeaderColumns);
- $this->CheckColumns($this->aMappedFields, [], 'csv file');
-
- if ($this->bHasHeader) {
- $this->iIdx++;
- /** NextLineObject**/
- $oNextLineArr = $this->getNextLine();
- }
- }
-
- $iColumnSize = count($this->aMappingCsvColumnIndexToFields);
- $iLineSize = count($oNextLineArr->getValues());
- if ($iColumnSize !== $iLineSize) {
- $line = $this->iIdx + 1;
- Utils::Log(LOG_ERR,
- "[".get_class($this)."] Wrong number of columns ($iLineSize) on line $line (expected $iColumnSize columns just like in header): ".$oNextLineArr->getCsvLine());
- throw new Exception("Invalid CSV file.");
- }
-
- $aData = array();
-
- foreach ($oNextLineArr->getValues() as $i =>$sVal) {
- $aSynchroFields = $this->aMappingCsvColumnIndexToFields[$i];
+ }
+
+ try {
+ $hHandle = fopen($sCsvFilePath, "r");
+ } catch (Exception $e) {
+ Utils::Log(LOG_INFO, "[".get_class($this)."] Cannot open CSV file $sCsvFilePath");
+ $sCsvFilePath = APPROOT.$sCsvFilePath;
+ try {
+ $hHandle = fopen($sCsvFilePath, "r");
+ } catch (Exception $e) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Cannot open CSV file $sCsvFilePath");
+
+ return false;
+ }
+ }
+
+ if (!$hHandle) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Cannot use CSV file handle for $sCsvFilePath");
+
+ return false;
+ }
+
+ $sTmpFile = tempnam(sys_get_temp_dir(), "decoded_");
+ file_put_contents($sTmpFile, iconv($this->sCsvEncoding, $this->GetCharset(), stream_get_contents($hHandle)));
+ $oTmpHandle = fopen($sTmpFile, "r");
+
+ while (($aData = fgetcsv($oTmpHandle, 0, $this->sCsvSeparator)) !== false) {
+ $this->aCsvFieldsPerLine[] = $aData;
+ }
+
+ fclose($oTmpHandle);
+ unlink($sTmpFile);
+
+ return $bRet;
+ }
+
+ /**
+ * @return NextLineObject
+ */
+ public function getNextLine()
+ {
+ $aValues = $this->aCsvFieldsPerLine[$this->iIdx];
+ $sCsvLine = implode($this->sCsvSeparator, $aValues);
+
+ return new NextLineObject($sCsvLine, $aValues);
+ }
+
+ /**
+ * Fetches one csv row at a time
+ * The first row is used to check if the columns of the result match the expected "fields"
+ *
+ * @see Collector::Fetch()
+ * @throws Exception
+ */
+ public function Fetch()
+ {
+ $iCount = count($this->aCsvFieldsPerLine);
+ if (($iCount == 0) || (($iCount == 1) && $this->bHasHeader)) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] CSV file is empty. Data collection stops here.");
+
+ return false;
+ }
+ if ($this->iIdx >= $iCount) {
+
+ return false;
+ }
+
+ /** NextLineObject**/
+ $oNextLineArr = $this->getNextLine();
+
+ if (!$this->aMappingCsvColumnIndexToFields) {
+ $aCsvHeaderColumns = $oNextLineArr->getValues();
+
+ $this->Configure($aCsvHeaderColumns);
+ $this->CheckColumns($this->aMappedFields, [], 'csv file');
+
+ if ($this->bHasHeader) {
+ $this->iIdx++;
+ /** NextLineObject**/
+ $oNextLineArr = $this->getNextLine();
+ }
+ }
+
+ $iColumnSize = count($this->aMappingCsvColumnIndexToFields);
+ $iLineSize = count($oNextLineArr->getValues());
+ if ($iColumnSize !== $iLineSize) {
+ $line = $this->iIdx + 1;
+ Utils::Log(
+ LOG_ERR,
+ "[".get_class($this)."] Wrong number of columns ($iLineSize) on line $line (expected $iColumnSize columns just like in header): ".$oNextLineArr->getCsvLine()
+ );
+ throw new Exception("Invalid CSV file.");
+ }
+
+ $aData = [];
+
+ foreach ($oNextLineArr->getValues() as $i => $sVal) {
+ $aSynchroFields = $this->aMappingCsvColumnIndexToFields[$i];
foreach ($aSynchroFields as $sSynchroField) {
if (array_key_exists($sSynchroField, $this->aSynchroFieldsToDefaultValues)) {
if (empty($sVal)) {
@@ -278,33 +287,33 @@ public function Fetch()
}
}
- }
-
- foreach ($this->aSynchroFieldsToDefaultValues as $sAttributeId => $sAttributeValue) {
- if (!array_key_exists($sAttributeId, $aData)) {
- $aData[$sAttributeId] = $sAttributeValue;
- }
- }
-
- $this->iIdx++;
-
- return $aData;
- }
-
- /**
- * @param $aCsvHeaderColumns
- */
- protected function Configure($aCsvHeaderColumns)
- {
- if ($this->bHasHeader) {
- $this->aMappingCsvColumnIndexToFields = [];
-
- foreach ($aCsvHeaderColumns as $sCsvColumn) {
- if (array_key_exists($sCsvColumn, $this->aMappingCsvColumnNameToFields)) {
- //use mapping instead of csv header sSynchroColumn
- $this->aMappingCsvColumnIndexToFields[] = $this->aMappingCsvColumnNameToFields[$sCsvColumn];
- } else {
- if(!array_key_exists($sCsvColumn, $this->aMappedFields)) {
+ }
+
+ foreach ($this->aSynchroFieldsToDefaultValues as $sAttributeId => $sAttributeValue) {
+ if (!array_key_exists($sAttributeId, $aData)) {
+ $aData[$sAttributeId] = $sAttributeValue;
+ }
+ }
+
+ $this->iIdx++;
+
+ return $aData;
+ }
+
+ /**
+ * @param $aCsvHeaderColumns
+ */
+ protected function Configure($aCsvHeaderColumns)
+ {
+ if ($this->bHasHeader) {
+ $this->aMappingCsvColumnIndexToFields = [];
+
+ foreach ($aCsvHeaderColumns as $sCsvColumn) {
+ if (array_key_exists($sCsvColumn, $this->aMappingCsvColumnNameToFields)) {
+ //use mapping instead of csv header sSynchroColumn
+ $this->aMappingCsvColumnIndexToFields[] = $this->aMappingCsvColumnNameToFields[$sCsvColumn];
+ } else {
+ if (!array_key_exists($sCsvColumn, $this->aMappedFields)) {
$this->aMappingCsvColumnIndexToFields[] = [$sCsvColumn];
$this->aMappingCsvColumnNameToFields[$sCsvColumn] = [$sCsvColumn];
$this->aMappedFields[$sCsvColumn] = '';
@@ -312,69 +321,71 @@ protected function Configure($aCsvHeaderColumns)
$this->aMappingCsvColumnIndexToFields[] = [''];
$this->aMappingCsvColumnNameToFields[$sCsvColumn] = [''];
}
- }
- }
- } else {
+ }
+ }
+ } else {
foreach ($this->aConfiguredHeaderColumns as $sSynchroField => $sCsvColumn) {
- $this->aMappingCsvColumnIndexToFields[$sCsvColumn-1][] = $sSynchroField;
+ $this->aMappingCsvColumnIndexToFields[$sCsvColumn - 1][] = $sSynchroField;
$this->aMappedFields[$sSynchroField] = '';
}
- foreach ( $this->aIgnoredCsvColumns as $sCsvColumn) {
- $this->aMappingCsvColumnIndexToFields[$sCsvColumn-1] = ['ignored_attribute_'.$sCsvColumn];
+ foreach ($this->aIgnoredCsvColumns as $sCsvColumn) {
+ $this->aMappingCsvColumnIndexToFields[$sCsvColumn - 1] = ['ignored_attribute_'.$sCsvColumn];
+ }
+ }
+ foreach ($this->aIgnoredCsvColumns as $sIgnoredCsvColumn) {
+ $this->aIgnoredSynchroFields = array_merge($this->aIgnoredSynchroFields, ($this->bHasHeader) ? $this->aMappingCsvColumnNameToFields[$sIgnoredCsvColumn] : $this->aMappingCsvColumnIndexToFields[$sIgnoredCsvColumn - 1]);
+ }
+
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource)
+ {
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Columns [".var_export($aSynchroColumns, true)."]");
+ foreach ($this->aFields as $sField => $aDefs) {
+ foreach ($aDefs['columns'] as $sSynchroColumn) {
+ if (array_key_exists($sSynchroColumn, $this->aSynchroFieldsToDefaultValues) || in_array($sSynchroColumn, $this->aIgnoredSynchroFields)) {
+ $aColumnsToIgnore[] = $sField;
+ }
}
}
- foreach ($this->aIgnoredCsvColumns as $sIgnoredCsvColumn) {
- $this->aIgnoredSynchroFields = array_merge( $this->aIgnoredSynchroFields, ($this->bHasHeader) ? $this->aMappingCsvColumnNameToFields[$sIgnoredCsvColumn] : $this->aMappingCsvColumnIndexToFields[$sIgnoredCsvColumn - 1]);
- }
-
- }
-
- /**
- * @inheritdoc
- */
- protected function CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource)
- {
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Columns [".var_export($aSynchroColumns, true)."]");
- foreach ($this->aFields as $sField => $aDefs) foreach ($aDefs['columns'] as $sSynchroColumn) {
- if (array_key_exists($sSynchroColumn, $this->aSynchroFieldsToDefaultValues) || in_array($sSynchroColumn, $this->aIgnoredSynchroFields)) {
- $aColumnsToIgnore[] = $sField;
- }
- }
-
- parent::CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource);
- }
+
+ parent::CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource);
+ }
}
class NextLineObject
{
- private $sCsvLine;
- private $aValues;
-
- /**
- * NextLineObject constructor.
- *
- * @param $csv_line
- * @param $aValues
- */
- public function __construct($sCsvLine, $aValues)
- {
- $this->sCsvLine = $sCsvLine;
- $this->aValues = $aValues;
- }
-
- /**
- * @return mixed
- */
- public function getCsvLine()
- {
- return $this->sCsvLine;
- }
-
- /**
- * @return mixed
- */
- public function getValues()
- {
- return $this->aValues;
- }
+ private $sCsvLine;
+ private $aValues;
+
+ /**
+ * NextLineObject constructor.
+ *
+ * @param $csv_line
+ * @param $aValues
+ */
+ public function __construct($sCsvLine, $aValues)
+ {
+ $this->sCsvLine = $sCsvLine;
+ $this->aValues = $aValues;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getCsvLine()
+ {
+ return $this->sCsvLine;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getValues()
+ {
+ return $this->aValues;
+ }
}
diff --git a/core/dopostrequestservice.class.inc.php b/core/dopostrequestservice.class.inc.php
index 2b31d42..3e37a91 100644
--- a/core/dopostrequestservice.class.inc.php
+++ b/core/dopostrequestservice.class.inc.php
@@ -5,7 +5,8 @@
*/
class DoPostRequestService
{
- public function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = []){
- return null;
- }
+ public function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = [])
+ {
+ return null;
+ }
}
diff --git a/core/ioexception.class.inc.php b/core/ioexception.class.inc.php
index d8bbc78..8fa4be8 100644
--- a/core/ioexception.class.inc.php
+++ b/core/ioexception.class.inc.php
@@ -2,4 +2,4 @@
class IOException extends Exception
{
-}
\ No newline at end of file
+}
diff --git a/core/jsoncollector.class.inc.php b/core/jsoncollector.class.inc.php
index 64baca8..52c326e 100644
--- a/core/jsoncollector.class.inc.php
+++ b/core/jsoncollector.class.inc.php
@@ -33,332 +33,331 @@
*/
abstract class JsonCollector extends Collector
{
- protected $sFileJson;
- protected $aJson;
- protected $sURL;
- protected $sFilePath;
- protected $aJsonKey;
- protected $aFieldsKey;
- protected $sJsonCliCommand;
- protected $iIdx;
- protected $aSynchroFieldsToDefaultValues = array();
-
- /**
- * Initalization
- */
- public function __construct()
- {
- parent::__construct();
- $this->sFileJson = null;
- $this->sURL = null;
- $this->aJson = null;
- $this->aFieldsKey = null;
- $this->iIdx = 0;
- }
-
- /**
- * Runs the configured query to start fetching the data from the database
- *
- * @see Collector::Prepare()
- */
- public function Prepare()
- {
- $bRet = parent::Prepare();
- if (!$bRet) {
- return false;
- }
-
- //**** step 1 : get all parameters from config file
- $aParamsSourceJson = $this->aCollectorConfig;
- if (isset($aParamsSourceJson["command"])) {
- $this->sJsonCliCommand = $aParamsSourceJson["command"];
- }
- if (isset($aParamsSourceJson["COMMAND"])) {
- $this->sJsonCliCommand = $aParamsSourceJson["COMMAND"];
- Utils::Log(LOG_INFO, "[".get_class($this)."] CLI command used is [".$this->sJsonCliCommand."]");
- }
-
-
- // Read the URL or Path from the configuration
- if (isset($aParamsSourceJson["jsonurl"])) {
- $this->sURL = $aParamsSourceJson["jsonurl"];
- }
- if (isset($aParamsSourceJson["JSONURL"])) {
- $this->sURL = $aParamsSourceJson["JSONURL"];
- }
-
- if ($this->sURL == '') {
- if (isset($aParamsSourceJson["jsonfile"])) {
- $this->sFilePath = $aParamsSourceJson["jsonfile"];
- }
- if (isset($aParamsSourceJson["JSONFILE"])) { // Try all lowercase
- $this->sFilePath = $aParamsSourceJson["JSONFILE"];
- }
- Utils::Log(LOG_INFO, "Source file path: ".$this->sFilePath);
- } else {
- Utils::Log(LOG_INFO, "Source URL: ".$this->sURL);
- }
-
- if ($this->sURL == '' && $this->sFilePath == '') {
- // No query at all !!
- Utils::Log(LOG_ERR, "[".get_class($this)."] no json URL or path configured! Cannot collect data. Please configure it as '' or '' in the configuration file.");
-
- return false;
- }
- if (array_key_exists('defaults', $aParamsSourceJson)) {
- if ($aParamsSourceJson['defaults'] !== '') {
- $this->aSynchroFieldsToDefaultValues = $aParamsSourceJson['defaults'];
- if (!is_array($this->aSynchroFieldsToDefaultValues)) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] defaults section configuration is not correct. please see documentation.");
-
- return false;
- }
- }
- }
-
- $aPath=[];
- if (isset($aParamsSourceJson["path"])) {
- $aPath = explode('/', $aParamsSourceJson["path"]);
- }
- if (isset($aParamsSourceJson["PATH"])) {
- $aPath = explode('/', $aParamsSourceJson["PATH"]);
- }
- if (count($aPath) == 0) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] no path to find data in JSON file");
- }
-
- //**** step 2 : get json file
- //execute cmd before get the json
- if (!empty($this->sJsonCliCommand)) {
- Utils::Exec($this->sJsonCliCommand);
- }
-
- //get Json file
- if ($this->sURL != '') {
- Utils::Log(LOG_DEBUG, 'Get params for uploading data file ');
+ protected $sFileJson;
+ protected $aJson;
+ protected $sURL;
+ protected $sFilePath;
+ protected $aJsonKey;
+ protected $aFieldsKey;
+ protected $sJsonCliCommand;
+ protected $iIdx;
+ protected $aSynchroFieldsToDefaultValues = [];
+
+ /**
+ * Initalization
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ $this->sFileJson = null;
+ $this->sURL = null;
+ $this->aJson = null;
+ $this->aFieldsKey = null;
+ $this->iIdx = 0;
+ }
+
+ /**
+ * Runs the configured query to start fetching the data from the database
+ *
+ * @see Collector::Prepare()
+ */
+ public function Prepare()
+ {
+ $bRet = parent::Prepare();
+ if (!$bRet) {
+ return false;
+ }
+
+ //**** step 1 : get all parameters from config file
+ $aParamsSourceJson = $this->aCollectorConfig;
+ if (isset($aParamsSourceJson["command"])) {
+ $this->sJsonCliCommand = $aParamsSourceJson["command"];
+ }
+ if (isset($aParamsSourceJson["COMMAND"])) {
+ $this->sJsonCliCommand = $aParamsSourceJson["COMMAND"];
+ Utils::Log(LOG_INFO, "[".get_class($this)."] CLI command used is [".$this->sJsonCliCommand."]");
+ }
+
+ // Read the URL or Path from the configuration
+ if (isset($aParamsSourceJson["jsonurl"])) {
+ $this->sURL = $aParamsSourceJson["jsonurl"];
+ }
+ if (isset($aParamsSourceJson["JSONURL"])) {
+ $this->sURL = $aParamsSourceJson["JSONURL"];
+ }
+
+ if ($this->sURL == '') {
+ if (isset($aParamsSourceJson["jsonfile"])) {
+ $this->sFilePath = $aParamsSourceJson["jsonfile"];
+ }
+ if (isset($aParamsSourceJson["JSONFILE"])) { // Try all lowercase
+ $this->sFilePath = $aParamsSourceJson["JSONFILE"];
+ }
+ Utils::Log(LOG_INFO, "Source file path: ".$this->sFilePath);
+ } else {
+ Utils::Log(LOG_INFO, "Source URL: ".$this->sURL);
+ }
+
+ if ($this->sURL == '' && $this->sFilePath == '') {
+ // No query at all !!
+ Utils::Log(LOG_ERR, "[".get_class($this)."] no json URL or path configured! Cannot collect data. Please configure it as '' or '' in the configuration file.");
+
+ return false;
+ }
+ if (array_key_exists('defaults', $aParamsSourceJson)) {
+ if ($aParamsSourceJson['defaults'] !== '') {
+ $this->aSynchroFieldsToDefaultValues = $aParamsSourceJson['defaults'];
+ if (!is_array($this->aSynchroFieldsToDefaultValues)) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] defaults section configuration is not correct. please see documentation.");
+
+ return false;
+ }
+ }
+ }
+
+ $aPath = [];
+ if (isset($aParamsSourceJson["path"])) {
+ $aPath = explode('/', $aParamsSourceJson["path"]);
+ }
+ if (isset($aParamsSourceJson["PATH"])) {
+ $aPath = explode('/', $aParamsSourceJson["PATH"]);
+ }
+ if (count($aPath) == 0) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] no path to find data in JSON file");
+ }
+
+ //**** step 2 : get json file
+ //execute cmd before get the json
+ if (!empty($this->sJsonCliCommand)) {
+ Utils::Exec($this->sJsonCliCommand);
+ }
+
+ //get Json file
+ if ($this->sURL != '') {
+ Utils::Log(LOG_DEBUG, 'Get params for uploading data file ');
$aDataGet = [];
- if (isset($aParamsSourceJson["jsonpost"])) {
- $aDataGet = $aParamsSourceJson['jsonpost'];
- } else {
- $aDataGet = [];
- }
- $iSynchroTimeout = (int)Utils::GetConfigurationValue('itop_synchro_timeout', 600); // timeout in seconds, for a synchro to run
- $aCurlOptions = Utils::GetCurlOptions($iSynchroTimeout);
-
- //logs
- Utils::Log(LOG_DEBUG, 'Source aDataGet: '.json_encode($aDataGet));
- $this->sFileJson = Utils::DoPostRequest($this->sURL, $aDataGet, '', $aResponseHeaders, $aCurlOptions);
- Utils::Log(LOG_DEBUG, 'Source sFileJson: '.$this->sFileJson);
- Utils::Log(LOG_INFO, 'Synchro URL (target): '.Utils::GetConfigurationValue('itop_url', array()));
- } else {
+ if (isset($aParamsSourceJson["jsonpost"])) {
+ $aDataGet = $aParamsSourceJson['jsonpost'];
+ } else {
+ $aDataGet = [];
+ }
+ $iSynchroTimeout = (int)Utils::GetConfigurationValue('itop_synchro_timeout', 600); // timeout in seconds, for a synchro to run
+ $aCurlOptions = Utils::GetCurlOptions($iSynchroTimeout);
+
+ //logs
+ Utils::Log(LOG_DEBUG, 'Source aDataGet: '.json_encode($aDataGet));
+ $this->sFileJson = Utils::DoPostRequest($this->sURL, $aDataGet, '', $aResponseHeaders, $aCurlOptions);
+ Utils::Log(LOG_DEBUG, 'Source sFileJson: '.$this->sFileJson);
+ Utils::Log(LOG_INFO, 'Synchro URL (target): '.Utils::GetConfigurationValue('itop_url', []));
+ } else {
$this->sFileJson = @file_get_contents($this->sFilePath);
if ($this->sFileJson === false) {
$this->sFilePath = APPROOT.$this->sFilePath;
$this->sFileJson = @file_get_contents($this->sFilePath);
}
- Utils::Log(LOG_DEBUG, 'Source sFileJson: '.$this->sFileJson);
- Utils::Log(LOG_INFO, 'Synchro URL (target): '.Utils::GetConfigurationValue('itop_url', array()));
- }
-
- //verify the file
- if ($this->sFileJson === false) {
- Utils::Log(LOG_ERR, '['.get_class($this).'] Failed to get JSON file: '.$this->sURL);
-
- return false;
- }
-
-
- //**** step 3 : read json file
- $this->aJson = json_decode($this->sFileJson, true);
- if ($this->aJson == null) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to translate data from JSON file: '".$this->sURL.$this->sFilePath."'. Reason: ".json_last_error_msg());
-
- return false;
- }
-
- //Get table of Element in JSON file with a specific path
- foreach ($aPath as $sTag) {
- Utils::Log(LOG_DEBUG, "tag: ".$sTag);
- //!array_key_exists(0, $this->aJson) => element $this->aJson is not a classic array It's an array with defined keys
- if (!array_key_exists(0, $this->aJson) && $sTag != '*') {
- $this->aJson = $this->aJson[$sTag];
- } else {
- $aJsonNew = array();
- foreach ($this->aJson as $aElement) {
- if ($sTag == '*') //Any tag
- {
- array_push($aJsonNew, $aElement);
- } else {
- if (isset($aElement[$sTag])) {
- array_push($aJsonNew, $aElement[$sTag]);
- }
- }
- }
- $this->aJson = $aJsonNew;
- }
- if (count($this->aJson) == 0) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to find path ".implode("/", $aPath)." until data in json file: $this->sURL $this->sFilePath.");
-
- return false;
- }
- }
- $this->aJsonKey = array_keys($this->aJson);
- if (isset($aParamsSourceJson["fields"])) {
- $this->aFieldsKey = $aParamsSourceJson["fields"];
- }
- if (isset($aParamsSourceJson["FIELDS"])) {
- $this->aFieldsKey = $aParamsSourceJson["FIELDS"];
- }
- Utils::Log(LOG_DEBUG, "aFieldsKey: ".json_encode($this->aFieldsKey));
- Utils::Log(LOG_DEBUG, "aJson: ".json_encode($this->aJson));
- Utils::Log(LOG_DEBUG, "aJsonKey: ".json_encode($this->aJsonKey));
- Utils::Log(LOG_DEBUG, "nb of elements:".count($this->aJson));
-
- $this->iIdx = 0;
-
- return true;
- }
-
- /**
- * Fetch one element from the JSON file
- * The first element is used to check if the columns of the result match the expected "fields"
- *
- * @see Collector::Fetch()
- */
- public function Fetch()
- {
- if (empty($this->aJson)) {
- return false;
- }
- if ($this->iIdx < count($this->aJson)) {
- $aData = $this->aJson[$this->aJsonKey[$this->iIdx]];
- Utils::Log(LOG_DEBUG, '$aData: '.json_encode($aData));
-
- $aDataToSynchronize = $this->SearchFieldValues($aData);
-
- foreach ($this->aSkippedAttributes as $sCode) {
- unset($aDataToSynchronize[$sCode]);
- }
-
- if ($this->iIdx == 0) {
- $this->CheckColumns($aDataToSynchronize, [], 'json file');
- }
- //check if all expected fields are in array. If not add it with null value
- foreach ($this->aCSVHeaders as $sHeader) {
- if (!isset($aDataToSynchronize[$sHeader])) {
- $aDataToSynchronize[$sHeader] = null;
- }
- }
-
- foreach ($this->aNullifiedAttributes as $sHeader) {
- if (!isset($aDataToSynchronize[$sHeader])) {
- $aDataToSynchronize[$sHeader] = null;
- }
- }
-
- $this->iIdx++;
-
- return $aDataToSynchronize;
- }
-
- return false;
- }
-
- /**
- * @param array $aData
- *
- * @return array
- * @throws \Exception
- */
- private function SearchFieldValues($aData, $aTestOnlyFieldsKey=null) {
- $aDataToSynchronize = [];
-
- $aCurrentFieldKeys = (is_null($aTestOnlyFieldsKey)) ? $this->aFieldsKey : $aTestOnlyFieldsKey;
- foreach ($aCurrentFieldKeys as $key => $sPath) {
- if ($this->iIdx == 0) {
- Utils::Log(LOG_DEBUG, $key.":".array_search($key, $aCurrentFieldKeys));
- }
- //
- $aJsonKeyPath = explode('/', $sPath);
- $aValue = $this->SearchValue($aJsonKeyPath, $aData);
-
- if (empty($aValue) && array_key_exists($key, $this->aSynchroFieldsToDefaultValues)){
- $sDefaultValue = $this->aSynchroFieldsToDefaultValues[$key];
- Utils::Log(LOG_DEBUG, "aDataToSynchronize[$key]: $sDefaultValue");
- $aDataToSynchronize[$key] = $sDefaultValue;
- } else if (! is_null($aValue)){
- Utils::Log(LOG_DEBUG, "aDataToSynchronize[$key]: $aValue");
- $aDataToSynchronize[$key] = $aValue;
- }
- }
-
- Utils::Log(LOG_DEBUG, '$aDataToSynchronize: '.json_encode($aDataToSynchronize));
- return $aDataToSynchronize;
- }
-
- private function SearchValue($aJsonKeyPath, $aData){
- $sTag = array_shift($aJsonKeyPath);
-
- if($sTag === '*'){
- foreach ($aData as $sKey => $aDataValue){
- $aCurrentValue = $this->SearchValue($aJsonKeyPath, $aDataValue);
- if (null !== $aCurrentValue){
- return $aCurrentValue;
- }
- }
- return null;
- }
-
- if (is_int($sTag)
- && array_is_list($aData)
- && array_key_exists((int) $sTag, $aData)
- ) {
- $aValue = $aData[(int) $sTag];
- } else if(($sTag != '*')
- && is_array($aData)
- && isset($aData[$sTag])
- ){
- $aValue = $aData[$sTag];
- } else {
- return null;
- }
-
- if (empty($aJsonKeyPath)){
- return (is_array($aValue)) ? null : $aValue;
- }
-
- return $this->SearchValue($aJsonKeyPath, $aValue);
- }
-
- /**
- * Determine if a given attribute is allowed to be missing in the data datamodel.
- *
- * The implementation is based on a predefined configuration parameter named from the
- * class of the collector (all lowercase) with _ignored_attributes appended.
- *
- * Example: here is the configuration to "ignore" the attribute 'location_id' for the class MyJSONCollector:
- *
- * location_id
- *
- *
- * @param string $sAttCode
- *
- * @return boolean True if the attribute can be skipped, false otherwise
- */
- public function AttributeIsOptional($sAttCode)
- {
- $aIgnoredAttributes = Utils::GetConfigurationValue(get_class($this)."_ignored_attributes", null);
- if ($aIgnoredAttributes === null) {
- // Try all lowercase
- $aIgnoredAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_ignored_attributes", null);
- }
- if (is_array($aIgnoredAttributes)) {
- if (in_array($sAttCode, $aIgnoredAttributes)) {
- return true;
- }
- }
-
- return parent::AttributeIsOptional($sAttCode);
- }
+ Utils::Log(LOG_DEBUG, 'Source sFileJson: '.$this->sFileJson);
+ Utils::Log(LOG_INFO, 'Synchro URL (target): '.Utils::GetConfigurationValue('itop_url', []));
+ }
+
+ //verify the file
+ if ($this->sFileJson === false) {
+ Utils::Log(LOG_ERR, '['.get_class($this).'] Failed to get JSON file: '.$this->sURL);
+
+ return false;
+ }
+
+ //**** step 3 : read json file
+ $this->aJson = json_decode($this->sFileJson, true);
+ if ($this->aJson == null) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to translate data from JSON file: '".$this->sURL.$this->sFilePath."'. Reason: ".json_last_error_msg());
+
+ return false;
+ }
+
+ //Get table of Element in JSON file with a specific path
+ foreach ($aPath as $sTag) {
+ Utils::Log(LOG_DEBUG, "tag: ".$sTag);
+ //!array_key_exists(0, $this->aJson) => element $this->aJson is not a classic array It's an array with defined keys
+ if (!array_key_exists(0, $this->aJson) && $sTag != '*') {
+ $this->aJson = $this->aJson[$sTag];
+ } else {
+ $aJsonNew = [];
+ foreach ($this->aJson as $aElement) {
+ if ($sTag == '*') { //Any tag
+ array_push($aJsonNew, $aElement);
+ } else {
+ if (isset($aElement[$sTag])) {
+ array_push($aJsonNew, $aElement[$sTag]);
+ }
+ }
+ }
+ $this->aJson = $aJsonNew;
+ }
+ if (count($this->aJson) == 0) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to find path ".implode("/", $aPath)." until data in json file: $this->sURL $this->sFilePath.");
+
+ return false;
+ }
+ }
+ $this->aJsonKey = array_keys($this->aJson);
+ if (isset($aParamsSourceJson["fields"])) {
+ $this->aFieldsKey = $aParamsSourceJson["fields"];
+ }
+ if (isset($aParamsSourceJson["FIELDS"])) {
+ $this->aFieldsKey = $aParamsSourceJson["FIELDS"];
+ }
+ Utils::Log(LOG_DEBUG, "aFieldsKey: ".json_encode($this->aFieldsKey));
+ Utils::Log(LOG_DEBUG, "aJson: ".json_encode($this->aJson));
+ Utils::Log(LOG_DEBUG, "aJsonKey: ".json_encode($this->aJsonKey));
+ Utils::Log(LOG_DEBUG, "nb of elements:".count($this->aJson));
+
+ $this->iIdx = 0;
+
+ return true;
+ }
+
+ /**
+ * Fetch one element from the JSON file
+ * The first element is used to check if the columns of the result match the expected "fields"
+ *
+ * @see Collector::Fetch()
+ */
+ public function Fetch()
+ {
+ if (empty($this->aJson)) {
+ return false;
+ }
+ if ($this->iIdx < count($this->aJson)) {
+ $aData = $this->aJson[$this->aJsonKey[$this->iIdx]];
+ Utils::Log(LOG_DEBUG, '$aData: '.json_encode($aData));
+
+ $aDataToSynchronize = $this->SearchFieldValues($aData);
+
+ foreach ($this->aSkippedAttributes as $sCode) {
+ unset($aDataToSynchronize[$sCode]);
+ }
+
+ if ($this->iIdx == 0) {
+ $this->CheckColumns($aDataToSynchronize, [], 'json file');
+ }
+ //check if all expected fields are in array. If not add it with null value
+ foreach ($this->aCSVHeaders as $sHeader) {
+ if (!isset($aDataToSynchronize[$sHeader])) {
+ $aDataToSynchronize[$sHeader] = null;
+ }
+ }
+
+ foreach ($this->aNullifiedAttributes as $sHeader) {
+ if (!isset($aDataToSynchronize[$sHeader])) {
+ $aDataToSynchronize[$sHeader] = null;
+ }
+ }
+
+ $this->iIdx++;
+
+ return $aDataToSynchronize;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param array $aData
+ *
+ * @return array
+ * @throws \Exception
+ */
+ private function SearchFieldValues($aData, $aTestOnlyFieldsKey = null)
+ {
+ $aDataToSynchronize = [];
+
+ $aCurrentFieldKeys = (is_null($aTestOnlyFieldsKey)) ? $this->aFieldsKey : $aTestOnlyFieldsKey;
+ foreach ($aCurrentFieldKeys as $key => $sPath) {
+ if ($this->iIdx == 0) {
+ Utils::Log(LOG_DEBUG, $key.":".array_search($key, $aCurrentFieldKeys));
+ }
+ //
+ $aJsonKeyPath = explode('/', $sPath);
+ $aValue = $this->SearchValue($aJsonKeyPath, $aData);
+
+ if (empty($aValue) && array_key_exists($key, $this->aSynchroFieldsToDefaultValues)) {
+ $sDefaultValue = $this->aSynchroFieldsToDefaultValues[$key];
+ Utils::Log(LOG_DEBUG, "aDataToSynchronize[$key]: $sDefaultValue");
+ $aDataToSynchronize[$key] = $sDefaultValue;
+ } elseif (! is_null($aValue)) {
+ Utils::Log(LOG_DEBUG, "aDataToSynchronize[$key]: $aValue");
+ $aDataToSynchronize[$key] = $aValue;
+ }
+ }
+
+ Utils::Log(LOG_DEBUG, '$aDataToSynchronize: '.json_encode($aDataToSynchronize));
+ return $aDataToSynchronize;
+ }
+
+ private function SearchValue($aJsonKeyPath, $aData)
+ {
+ $sTag = array_shift($aJsonKeyPath);
+
+ if ($sTag === '*') {
+ foreach ($aData as $sKey => $aDataValue) {
+ $aCurrentValue = $this->SearchValue($aJsonKeyPath, $aDataValue);
+ if (null !== $aCurrentValue) {
+ return $aCurrentValue;
+ }
+ }
+ return null;
+ }
+
+ if (is_int($sTag)
+ && array_is_list($aData)
+ && array_key_exists((int) $sTag, $aData)
+ ) {
+ $aValue = $aData[(int) $sTag];
+ } elseif (($sTag != '*')
+ && is_array($aData)
+ && isset($aData[$sTag])
+ ) {
+ $aValue = $aData[$sTag];
+ } else {
+ return null;
+ }
+
+ if (empty($aJsonKeyPath)) {
+ return (is_array($aValue)) ? null : $aValue;
+ }
+
+ return $this->SearchValue($aJsonKeyPath, $aValue);
+ }
+
+ /**
+ * Determine if a given attribute is allowed to be missing in the data datamodel.
+ *
+ * The implementation is based on a predefined configuration parameter named from the
+ * class of the collector (all lowercase) with _ignored_attributes appended.
+ *
+ * Example: here is the configuration to "ignore" the attribute 'location_id' for the class MyJSONCollector:
+ *
+ * location_id
+ *
+ *
+ * @param string $sAttCode
+ *
+ * @return boolean True if the attribute can be skipped, false otherwise
+ */
+ public function AttributeIsOptional($sAttCode)
+ {
+ $aIgnoredAttributes = Utils::GetConfigurationValue(get_class($this)."_ignored_attributes", null);
+ if ($aIgnoredAttributes === null) {
+ // Try all lowercase
+ $aIgnoredAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_ignored_attributes", null);
+ }
+ if (is_array($aIgnoredAttributes)) {
+ if (in_array($sAttCode, $aIgnoredAttributes)) {
+ return true;
+ }
+ }
+
+ return parent::AttributeIsOptional($sAttCode);
+ }
}
diff --git a/core/lookuptable.class.inc.php b/core/lookuptable.class.inc.php
index c507225..be09347 100644
--- a/core/lookuptable.class.inc.php
+++ b/core/lookuptable.class.inc.php
@@ -1,7 +1,8 @@
[ WHERE ...]
- * @param array $aKeyFields The fields of the object to use in the lookup key
- * @param bool $bCaseSensitive Is the mapping case sensitive ?
- * @param bool $bIgnoreMappingErrors Are mapping errors considered as "normal"? (e.g. when using the lookup table for filtering the data)
- * @param string $sReturnAttCode The attribute code whose value to return as the result of the mapping (by default 'id' meaning the ID of the matching iTop object)
- *
- * @throws Exception
- */
- public function __construct($sOQL, $aKeyFields, $bCaseSensitive = true, $bIgnoreMappingErrors = false, $sReturnAttCode = 'id')
- {
- $this->aData = array();
- $this->aFieldsPos = array();
- $this->bCaseSensitive = $bCaseSensitive;
- $this->bIgnoreMappingErrors = $bIgnoreMappingErrors;
- $this->sReturnAttCode = $sReturnAttCode;
+ /**
+ * Initialization of a LookupTable, based on an OQL query in iTop
+ *
+ * @param string $sOQL The OQL query for the objects to integrate in the LookupTable. Format: SELECT [ WHERE ...]
+ * @param array $aKeyFields The fields of the object to use in the lookup key
+ * @param bool $bCaseSensitive Is the mapping case sensitive ?
+ * @param bool $bIgnoreMappingErrors Are mapping errors considered as "normal"? (e.g. when using the lookup table for filtering the data)
+ * @param string $sReturnAttCode The attribute code whose value to return as the result of the mapping (by default 'id' meaning the ID of the matching iTop object)
+ *
+ * @throws Exception
+ */
+ public function __construct($sOQL, $aKeyFields, $bCaseSensitive = true, $bIgnoreMappingErrors = false, $sReturnAttCode = 'id')
+ {
+ $this->aData = [];
+ $this->aFieldsPos = [];
+ $this->bCaseSensitive = $bCaseSensitive;
+ $this->bIgnoreMappingErrors = $bIgnoreMappingErrors;
+ $this->sReturnAttCode = $sReturnAttCode;
- if (!preg_match('/^SELECT ([^ ]+)/', $sOQL, $aMatches)) {
- throw new Exception("Invalid OQL query: '$sOQL'. Expecting a query starting with 'SELECT xxx'");
- }
- $sClass = $aMatches[1];
- if(static::$oRestClient != null) {
- $oRestClient = static::$oRestClient;
- } else {
- $oRestClient = new RestClient();
- }
- $aRestFields = $aKeyFields;
- if ($this->sReturnAttCode !== 'id') {
- // If the return attcode is not the ID of the object, add it to the list of the required fields
- $aRestFields[] = $this->sReturnAttCode;
- }
- $aRes = $oRestClient->Get($sClass, $sOQL, implode(',', $aRestFields));
- if ($aRes['code'] == 0) {
- foreach ((array)$aRes['objects'] as $sObjKey => $aObj) {
- $iObjKey = 0;
- $aMappingKeys = array();
- foreach ($aKeyFields as $sField) {
- if (!array_key_exists($sField, $aObj['fields'])) {
- Utils::Log(LOG_ERR, "field '$sField' does not exist in '".json_encode($aObj['fields'])."'");
- $aMappingKeys[] = '';
- } else {
- $aMappingKeys[] = $aObj['fields'][$sField];
- }
- }
- $sMappingKey = implode( '_', $aMappingKeys);
- if (!$this->bCaseSensitive) {
- if (function_exists('mb_strtolower')) {
- $sMappingKey = mb_strtolower($sMappingKey);
- } else {
- $sMappingKey = strtolower($sMappingKey);
- }
- }
- if ($this->sReturnAttCode !== 'id') {
- // If the return attcode is not the ID of the object, check that it exists
- if (!array_key_exists($this->sReturnAttCode, $aObj['fields'])) {
- Utils::Log(LOG_ERR, "field '{$this->sReturnAttCode}' does not exist in '".json_encode($aObj['fields'])."'");
- $iObjKey = 0;
- } else {
- $iObjKey = $aObj['fields'][$this->sReturnAttCode];
- }
- } else {
- // The return value is the ID of the object
- if (!array_key_exists('key', $aObj)) {
- // Emulate the behavior for older versions of the REST API
- if (preg_match('/::([0-9]+)$/', $sObjKey, $aMatches)) {
- $iObjKey = (int)$aMatches[1];
- }
- } else {
- $iObjKey = (int)$aObj['key'];
- }
- }
- $this->aData[$sMappingKey] = $iObjKey; // Store the mapping
- }
- } else {
- Utils::Log(LOG_ERR, "Unable to retrieve the $sClass objects (query = $sOQL). Message: ".$aRes['message']);
- }
- }
+ if (!preg_match('/^SELECT ([^ ]+)/', $sOQL, $aMatches)) {
+ throw new Exception("Invalid OQL query: '$sOQL'. Expecting a query starting with 'SELECT xxx'");
+ }
+ $sClass = $aMatches[1];
+ if (static::$oRestClient != null) {
+ $oRestClient = static::$oRestClient;
+ } else {
+ $oRestClient = new RestClient();
+ }
+ $aRestFields = $aKeyFields;
+ if ($this->sReturnAttCode !== 'id') {
+ // If the return attcode is not the ID of the object, add it to the list of the required fields
+ $aRestFields[] = $this->sReturnAttCode;
+ }
+ $aRes = $oRestClient->Get($sClass, $sOQL, implode(',', $aRestFields));
+ if ($aRes['code'] == 0) {
+ foreach ((array)$aRes['objects'] as $sObjKey => $aObj) {
+ $iObjKey = 0;
+ $aMappingKeys = [];
+ foreach ($aKeyFields as $sField) {
+ if (!array_key_exists($sField, $aObj['fields'])) {
+ Utils::Log(LOG_ERR, "field '$sField' does not exist in '".json_encode($aObj['fields'])."'");
+ $aMappingKeys[] = '';
+ } else {
+ $aMappingKeys[] = $aObj['fields'][$sField];
+ }
+ }
+ $sMappingKey = implode('_', $aMappingKeys);
+ if (!$this->bCaseSensitive) {
+ if (function_exists('mb_strtolower')) {
+ $sMappingKey = mb_strtolower($sMappingKey);
+ } else {
+ $sMappingKey = strtolower($sMappingKey);
+ }
+ }
+ if ($this->sReturnAttCode !== 'id') {
+ // If the return attcode is not the ID of the object, check that it exists
+ if (!array_key_exists($this->sReturnAttCode, $aObj['fields'])) {
+ Utils::Log(LOG_ERR, "field '{$this->sReturnAttCode}' does not exist in '".json_encode($aObj['fields'])."'");
+ $iObjKey = 0;
+ } else {
+ $iObjKey = $aObj['fields'][$this->sReturnAttCode];
+ }
+ } else {
+ // The return value is the ID of the object
+ if (!array_key_exists('key', $aObj)) {
+ // Emulate the behavior for older versions of the REST API
+ if (preg_match('/::([0-9]+)$/', $sObjKey, $aMatches)) {
+ $iObjKey = (int)$aMatches[1];
+ }
+ } else {
+ $iObjKey = (int)$aObj['key'];
+ }
+ }
+ $this->aData[$sMappingKey] = $iObjKey; // Store the mapping
+ }
+ } else {
+ Utils::Log(LOG_ERR, "Unable to retrieve the $sClass objects (query = $sOQL). Message: ".$aRes['message']);
+ }
+ }
- /**
- * Replaces the given field in the CSV data by the identifier of the object in iTop, based on a list of lookup fields
- *
- * @param hash $aLineData The data corresponding to the line of the CSV file being processed
- * @param array $aLookupFields The list of fields used for the mapping key
- * @param string $sDestField The name of field (i.e. column) to populate with the id of the iTop object
- * @param int $iLineIndex The index of the line (0 = first line of the CSV file)
- *
- * @return bool true if the mapping succeeded, false otherwise
- */
- public function Lookup(&$aLineData, $aLookupFields, $sDestField, $iLineIndex, $bSkipIfEmpty = false )
- {
- $bRet = true;
- if ($iLineIndex == 0) {
- $this->InitLineMappings($aLineData, array_merge($aLookupFields, array($sDestField)));
- } else {
- $iPos = $this->aFieldsPos[$sDestField];
- //skip search if field is empty
- if ($bSkipIfEmpty && $iPos !== null && $aLineData[$iPos] === '' ) {
- return false;
- }
- $aLookupKey = array();
- foreach ($aLookupFields as $sField) {
- $iPos = $this->aFieldsPos[$sField];
- if ($iPos !== null) {
- $aLookupKey[] = $aLineData[$iPos];
- } else {
- $aLookupKey[] = ''; // missing column ??
- }
- }
- $sLookupKey = implode('_', $aLookupKey);
- if (!$this->bCaseSensitive) {
- if (function_exists('mb_strtolower')) {
- $sLookupKey = mb_strtolower($sLookupKey);
- } else {
- $sLookupKey = strtolower($sLookupKey);
- }
- }
- if (!array_key_exists($sLookupKey, $this->aData)) {
- if ($this->bIgnoreMappingErrors) {
- // Mapping *errors* are expected, just report them in debug mode
- Utils::Log(LOG_DEBUG, "No mapping found with key: '$sLookupKey', '$sDestField' will be set to zero.");
- } else {
- Utils::Log(LOG_WARNING, "No mapping found with key: '$sLookupKey', '$sDestField' will be set to zero.");
- $bRet = false;
- }
- } else {
- $iPos = $this->aFieldsPos[$sDestField];
- if ($iPos !== null) {
- $aLineData[$iPos] = $this->aData[$sLookupKey];
- } else {
- Utils::Log(LOG_WARNING, "'$sDestField' is not a valid column name in the CSV file. Mapping will be ignored.");
- }
- }
- }
+ /**
+ * Replaces the given field in the CSV data by the identifier of the object in iTop, based on a list of lookup fields
+ *
+ * @param hash $aLineData The data corresponding to the line of the CSV file being processed
+ * @param array $aLookupFields The list of fields used for the mapping key
+ * @param string $sDestField The name of field (i.e. column) to populate with the id of the iTop object
+ * @param int $iLineIndex The index of the line (0 = first line of the CSV file)
+ *
+ * @return bool true if the mapping succeeded, false otherwise
+ */
+ public function Lookup(&$aLineData, $aLookupFields, $sDestField, $iLineIndex, $bSkipIfEmpty = false)
+ {
+ $bRet = true;
+ if ($iLineIndex == 0) {
+ $this->InitLineMappings($aLineData, array_merge($aLookupFields, [$sDestField]));
+ } else {
+ $iPos = $this->aFieldsPos[$sDestField];
+ //skip search if field is empty
+ if ($bSkipIfEmpty && $iPos !== null && $aLineData[$iPos] === '') {
+ return false;
+ }
+ $aLookupKey = [];
+ foreach ($aLookupFields as $sField) {
+ $iPos = $this->aFieldsPos[$sField];
+ if ($iPos !== null) {
+ $aLookupKey[] = $aLineData[$iPos];
+ } else {
+ $aLookupKey[] = ''; // missing column ??
+ }
+ }
+ $sLookupKey = implode('_', $aLookupKey);
+ if (!$this->bCaseSensitive) {
+ if (function_exists('mb_strtolower')) {
+ $sLookupKey = mb_strtolower($sLookupKey);
+ } else {
+ $sLookupKey = strtolower($sLookupKey);
+ }
+ }
+ if (!array_key_exists($sLookupKey, $this->aData)) {
+ if ($this->bIgnoreMappingErrors) {
+ // Mapping *errors* are expected, just report them in debug mode
+ Utils::Log(LOG_DEBUG, "No mapping found with key: '$sLookupKey', '$sDestField' will be set to zero.");
+ } else {
+ Utils::Log(LOG_WARNING, "No mapping found with key: '$sLookupKey', '$sDestField' will be set to zero.");
+ $bRet = false;
+ }
+ } else {
+ $iPos = $this->aFieldsPos[$sDestField];
+ if ($iPos !== null) {
+ $aLineData[$iPos] = $this->aData[$sLookupKey];
+ } else {
+ Utils::Log(LOG_WARNING, "'$sDestField' is not a valid column name in the CSV file. Mapping will be ignored.");
+ }
+ }
+ }
- return $bRet;
- }
+ return $bRet;
+ }
- /**
- * Initializes the mapping between the column names (given by the first line of the CSV) and their index, for the given columns
- *
- * @param hash $aLineHeaders An array of strings (the "headers" i.e. first line of the CSV file)
- * @param array $aFields The fields for which a mapping is requested, as an array of strings
- */
- protected function InitLineMappings($aLineHeaders, $aFields)
- {
- foreach ($aLineHeaders as $idx => $sHeader) {
- if (in_array($sHeader, $aFields)) {
- $this->aFieldsPos[$sHeader] = $idx;
- }
- }
+ /**
+ * Initializes the mapping between the column names (given by the first line of the CSV) and their index, for the given columns
+ *
+ * @param hash $aLineHeaders An array of strings (the "headers" i.e. first line of the CSV file)
+ * @param array $aFields The fields for which a mapping is requested, as an array of strings
+ */
+ protected function InitLineMappings($aLineHeaders, $aFields)
+ {
+ foreach ($aLineHeaders as $idx => $sHeader) {
+ if (in_array($sHeader, $aFields)) {
+ $this->aFieldsPos[$sHeader] = $idx;
+ }
+ }
- // Check that all requested fields were found in the headers
- foreach ($aFields as $sField) {
- if (!array_key_exists($sField, $this->aFieldsPos)) {
- Utils::Log(LOG_ERR, "'$sField' is not a valid column name in the CSV file. Mapping will fail.");
- }
- }
- }
+ // Check that all requested fields were found in the headers
+ foreach ($aFields as $sField) {
+ if (!array_key_exists($sField, $this->aFieldsPos)) {
+ Utils::Log(LOG_ERR, "'$sField' is not a valid column name in the CSV file. Mapping will fail.");
+ }
+ }
+ }
}
diff --git a/core/mappingtable.class.inc.php b/core/mappingtable.class.inc.php
index 1fdab8f..24f43be 100644
--- a/core/mappingtable.class.inc.php
+++ b/core/mappingtable.class.inc.php
@@ -1,7 +1,8 @@
- $this->sConfigEntryName = $sConfigEntryName;
- $aRawMapping = Utils::GetConfigurationValue($sConfigEntryName, array());
- foreach ($aRawMapping as $sExtendedPattern) {
- $sDelimiter = $sExtendedPattern[0];
- $iEndingDelimiterPos = strrpos($sExtendedPattern, $sDelimiter);
- $sPattern = substr($sExtendedPattern, 0, $iEndingDelimiterPos + 1);
- $sReplacement = substr($sExtendedPattern, $iEndingDelimiterPos + 1);
- $this->aMappingTable[] = array(
- 'pattern' => $sPattern,
- 'replacement' => $sReplacement,
- );
- }
- }
+ /**
+ * Creates a new MappingTable
+ *
+ * @param string $sConfigEntryName Name of the XML tag (in the params file) under which the configuration of the mapping table is stored
+ */
+ public function __construct($sConfigEntryName)
+ {
+ // Read the "extended mapping" from the configuration
+ // The mapping is expressed as an array of strings in the following format:
+ $this->sConfigEntryName = $sConfigEntryName;
+ $aRawMapping = Utils::GetConfigurationValue($sConfigEntryName, []);
+ foreach ($aRawMapping as $sExtendedPattern) {
+ $sDelimiter = $sExtendedPattern[0];
+ $iEndingDelimiterPos = strrpos($sExtendedPattern, $sDelimiter);
+ $sPattern = substr($sExtendedPattern, 0, $iEndingDelimiterPos + 1);
+ $sReplacement = substr($sExtendedPattern, $iEndingDelimiterPos + 1);
+ $this->aMappingTable[] = [
+ 'pattern' => $sPattern,
+ 'replacement' => $sReplacement,
+ ];
+ }
+ }
- /**
- * Normalizes a value through the mapping table
- *
- * @param string $sRawValue The value to normalize
- * @param string $defaultValue Default value if no match is found in the mapping table
- *
- * @return string The normalized value. Can be null if no match is found and no default value was supplied.
- */
- public function MapValue($sRawValue, $defaultValue = null)
- {
- $value = null;
- foreach ($this->aMappingTable as $aMapping) {
- if (preg_match($aMapping['pattern'].'iu', $sRawValue, $aMatches)) // 'i' for case insensitive matching, 'u' for utf-8 characters
- {
- $value = vsprintf($aMapping['replacement'], $aMatches); // found a suitable match
- Utils::Log(LOG_DEBUG, "MappingTable[{$this->sConfigEntryName}]: input value '$sRawValue' matches '{$aMapping['pattern']}'. Output value is '$value'");
- break;
- }
- }
- if ($value === null) {
- $value = $defaultValue;
- }
+ /**
+ * Normalizes a value through the mapping table
+ *
+ * @param string $sRawValue The value to normalize
+ * @param string $defaultValue Default value if no match is found in the mapping table
+ *
+ * @return string The normalized value. Can be null if no match is found and no default value was supplied.
+ */
+ public function MapValue($sRawValue, $defaultValue = null)
+ {
+ $value = null;
+ foreach ($this->aMappingTable as $aMapping) {
+ if (preg_match($aMapping['pattern'].'iu', $sRawValue, $aMatches)) { // 'i' for case insensitive matching, 'u' for utf-8 characters
+ $value = vsprintf($aMapping['replacement'], $aMatches); // found a suitable match
+ Utils::Log(LOG_DEBUG, "MappingTable[{$this->sConfigEntryName}]: input value '$sRawValue' matches '{$aMapping['pattern']}'. Output value is '$value'");
+ break;
+ }
+ }
+ if ($value === null) {
+ $value = $defaultValue;
+ }
- return $value;
- }
-}
\ No newline at end of file
+ return $value;
+ }
+}
diff --git a/core/orchestrator.class.inc.php b/core/orchestrator.class.inc.php
index 7a82860..bb4cd06 100644
--- a/core/orchestrator.class.inc.php
+++ b/core/orchestrator.class.inc.php
@@ -1,7 +1,8 @@
'7.0', 'simplexml' => '7.0', 'dom' => '1');
+ public static $aCollectors = [];
+ public static $aMinVersions = ['PHP' => '7.0', 'simplexml' => '7.0', 'dom' => '1'];
- /**
- * Add a collector class to be run in the specified order
- *
- * @param float $fExecOrder The execution order (smaller numbers run first)
- * @param string $sCollectorClass The class name of the collector. Must be a subclass of {@link Collector}
- *
- * @return void
- * @throws Exception
- */
- static function AddCollector($fExecOrder, $sCollectorClass)
- {
- $oReflection = new ReflectionClass($sCollectorClass);
- if (!$oReflection->IsSubclassOf('Collector')) {
- throw new Exception('Cannot register a collector class ('.$sCollectorClass.') which is not derived from Collector.');
- }
- if ($oReflection->IsAbstract()) {
- throw new Exception('Cannot register an abstract class ('.$sCollectorClass.') as a collector.');
- }
- self::$aCollectors[$sCollectorClass] = array('order' => $fExecOrder, 'class' => $sCollectorClass, 'sds_name' => '', 'sds_id' => 0);
- }
+ /**
+ * Add a collector class to be run in the specified order
+ *
+ * @param float $fExecOrder The execution order (smaller numbers run first)
+ * @param string $sCollectorClass The class name of the collector. Must be a subclass of {@link Collector}
+ *
+ * @return void
+ * @throws Exception
+ */
+ public static function AddCollector($fExecOrder, $sCollectorClass)
+ {
+ $oReflection = new ReflectionClass($sCollectorClass);
+ if (!$oReflection->IsSubclassOf('Collector')) {
+ throw new Exception('Cannot register a collector class ('.$sCollectorClass.') which is not derived from Collector.');
+ }
+ if ($oReflection->IsAbstract()) {
+ throw new Exception('Cannot register an abstract class ('.$sCollectorClass.') as a collector.');
+ }
+ self::$aCollectors[$sCollectorClass] = ['order' => $fExecOrder, 'class' => $sCollectorClass, 'sds_name' => '', 'sds_id' => 0];
+ }
- /**
- * @param string $sCollectionPlanClass The class name of the CollectionPlan. Must be a subclass of {@link CollectionPlan}
- *
- * @return void
- * @throws \ReflectionException
- */
- static function UseCollectionPlan($sCollectionPlanClass)
- {
- $oReflection = new ReflectionClass($sCollectionPlanClass);
- if (!$oReflection->IsSubclassOf(CollectionPlan::class)) {
- throw new Exception('Cannot register a CollectionPlan class ('.$sCollectionPlanClass.') which is not derived from CollectionPlan.');
- }
- if ($oReflection->IsAbstract()) {
- throw new Exception('Cannot register an CollectionPlan class ('.$sCollectionPlanClass.') as a CollectionPlan.');
- }
+ /**
+ * @param string $sCollectionPlanClass The class name of the CollectionPlan. Must be a subclass of {@link CollectionPlan}
+ *
+ * @return void
+ * @throws \ReflectionException
+ */
+ public static function UseCollectionPlan($sCollectionPlanClass)
+ {
+ $oReflection = new ReflectionClass($sCollectionPlanClass);
+ if (!$oReflection->IsSubclassOf(CollectionPlan::class)) {
+ throw new Exception('Cannot register a CollectionPlan class ('.$sCollectionPlanClass.') which is not derived from CollectionPlan.');
+ }
+ if ($oReflection->IsAbstract()) {
+ throw new Exception('Cannot register an CollectionPlan class ('.$sCollectionPlanClass.') as a CollectionPlan.');
+ }
/** @var CollectionPlan $oCollectionPlan */
- $oCollectionPlan = new $sCollectionPlanClass();
- $oCollectionPlan->Init();
- $oCollectionPlan->AddCollectorsToOrchestrator();
- }
+ $oCollectionPlan = new $sCollectionPlanClass();
+ $oCollectionPlan->Init();
+ $oCollectionPlan->AddCollectorsToOrchestrator();
+ }
- /**
- * Specify a requirement for a minimum version: either for PHP or for a specific extension
- *
- * @param string $sMinRequiredVersion The minimum version number required
- * @param string $sExtension The name of the extension, if not specified, then the requirement is for the PHP version itself
- *
- * @return void
- */
- static public function AddRequirement($sMinRequiredVersion, $sExtension = 'PHP')
- {
- if (!array_key_exists($sExtension, self::$aMinVersions)) {
- // This is the first call to add some requirements for this extension, record it as-is
+ /**
+ * Specify a requirement for a minimum version: either for PHP or for a specific extension
+ *
+ * @param string $sMinRequiredVersion The minimum version number required
+ * @param string $sExtension The name of the extension, if not specified, then the requirement is for the PHP version itself
+ *
+ * @return void
+ */
+ public static function AddRequirement($sMinRequiredVersion, $sExtension = 'PHP')
+ {
+ if (!array_key_exists($sExtension, self::$aMinVersions)) {
+ // This is the first call to add some requirements for this extension, record it as-is
+ self::$aMinVersions[$sExtension] = $sMinRequiredVersion;
+ } elseif (version_compare($sMinRequiredVersion, self::$aMinVersions[$sExtension], '>')) {
+ // This requirement is stricter than the previously requested one
self::$aMinVersions[$sExtension] = $sMinRequiredVersion;
- } elseif (version_compare($sMinRequiredVersion, self::$aMinVersions[$sExtension], '>')) {
- // This requirement is stricter than the previously requested one
- self::$aMinVersions[$sExtension] = $sMinRequiredVersion;
- }
- }
+ }
+ }
- /**
- * Check that all specified requirements are met, and log (LOG_ERR if not met, LOG_DEBUG if Ok)
- *
- * @return boolean True if it's Ok, false otherwise
- */
- static public function CheckRequirements()
- {
- $bResult = true;
- foreach (self::$aMinVersions as $sExtension => $sRequiredVersion) {
- if ($sExtension == 'PHP') {
- $sCurrentVersion = phpversion();
- if (version_compare($sCurrentVersion, $sRequiredVersion, '<')) {
- $bResult = false;
- Utils::Log(LOG_ERR, "The required PHP version to run this application is $sRequiredVersion. The current PHP version is only $sCurrentVersion.");
- } else {
- Utils::Log(LOG_DEBUG, "OK, the required PHP version to run this application is $sRequiredVersion. The current PHP version is $sCurrentVersion.");
- }
- } elseif (extension_loaded($sExtension)) {
- $sCurrentVersion = phpversion($sExtension);
- if (version_compare($sCurrentVersion, $sRequiredVersion, '<')) {
- $bResult = false;
- Utils::Log(LOG_ERR, "The extension '$sExtension' (version >= $sRequiredVersion) is required to run this application. The installed version is only $sCurrentVersion.");
- } else {
- Utils::Log(LOG_DEBUG, "OK, the required extension '$sExtension' is installed (current version: $sCurrentVersion >= $sRequiredVersion).");
- }
+ /**
+ * Check that all specified requirements are met, and log (LOG_ERR if not met, LOG_DEBUG if Ok)
+ *
+ * @return boolean True if it's Ok, false otherwise
+ */
+ public static function CheckRequirements()
+ {
+ $bResult = true;
+ foreach (self::$aMinVersions as $sExtension => $sRequiredVersion) {
+ if ($sExtension == 'PHP') {
+ $sCurrentVersion = phpversion();
+ if (version_compare($sCurrentVersion, $sRequiredVersion, '<')) {
+ $bResult = false;
+ Utils::Log(LOG_ERR, "The required PHP version to run this application is $sRequiredVersion. The current PHP version is only $sCurrentVersion.");
+ } else {
+ Utils::Log(LOG_DEBUG, "OK, the required PHP version to run this application is $sRequiredVersion. The current PHP version is $sCurrentVersion.");
+ }
+ } elseif (extension_loaded($sExtension)) {
+ $sCurrentVersion = phpversion($sExtension);
+ if (version_compare($sCurrentVersion, $sRequiredVersion, '<')) {
+ $bResult = false;
+ Utils::Log(LOG_ERR, "The extension '$sExtension' (version >= $sRequiredVersion) is required to run this application. The installed version is only $sCurrentVersion.");
+ } else {
+ Utils::Log(LOG_DEBUG, "OK, the required extension '$sExtension' is installed (current version: $sCurrentVersion >= $sRequiredVersion).");
+ }
- } else {
- $bResult = false;
- Utils::Log(LOG_ERR, "The missing extension '$sExtension' (version >= $sRequiredVersion) is required to run this application.");
- }
- }
+ } else {
+ $bResult = false;
+ Utils::Log(LOG_ERR, "The missing extension '$sExtension' (version >= $sRequiredVersion) is required to run this application.");
+ }
+ }
- return $bResult;
- }
+ return $bResult;
+ }
- /**
- * Returns the list of registered collectors, sorted in their execution order
- *
- * @return array An array of Collector instances
- */
- public function ListCollectors()
- {
- $aResults = array();
- //Sort the collectors based on their order
- uasort(self::$aCollectors, array("Orchestrator", "CompareCollectors"));
+ /**
+ * Returns the list of registered collectors, sorted in their execution order
+ *
+ * @return array An array of Collector instances
+ */
+ public function ListCollectors()
+ {
+ $aResults = [];
+ //Sort the collectors based on their order
+ uasort(self::$aCollectors, ["Orchestrator", "CompareCollectors"]);
- foreach (self::$aCollectors as $aCollectorData) {
+ foreach (self::$aCollectors as $aCollectorData) {
/** @var Collector $oClass */
- $oClass = new $aCollectorData['class']();
- $oClass->Init();
- $aResults[] = $oClass;
- //$aResults[] = new $aCollectorData['class']();
- }
+ $oClass = new $aCollectorData['class']();
+ $oClass->Init();
+ $aResults[] = $oClass;
+ //$aResults[] = new $aCollectorData['class']();
+ }
- return $aResults;
- }
+ return $aResults;
+ }
- /**
- * Initializes the synchronization data sources in iTop, according to the collectors' JSON specifications
- *
- * @param string[] $aCollectors list of classes implementing {@link Collector}
- *
- * @return boolean True if Ok, false otherwise
- * @throws \InvalidConfigException
- */
- public function InitSynchroDataSources($aCollectors)
- {
- $bResult = true;
- $aPlaceholders = array();
- $sEmailToNotify = Utils::GetConfigurationValue('contact_to_notify', '');
- $aPlaceholders['$contact_to_notify$'] = 0;
- if ($sEmailToNotify != '') {
- $oRestClient = new RestClient();
- $aRes = $oRestClient->Get('Contact', array('email' => $sEmailToNotify));
- if ($aRes['code'] == 0) {
- if (!is_array($aRes['objects'])) {
- Utils::Log(LOG_WARNING, "Contact to notify ($sEmailToNotify) not found in iTop. Nobody will be notified of the results of the synchronization.");
- } else {
- foreach ($aRes['objects'] as $sKey => $aObj) {
- if (!array_key_exists('key', $aObj)) {
- // Emulate the behavior for older versions of the API
- if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
- $aPlaceholders['$contact_to_notify$'] = (int)$aMatches[1];
- }
- } else {
- $aPlaceholders['$contact_to_notify$'] = (int)$aObj['key'];
- }
- Utils::Log(LOG_INFO, "Contact to notify: '{$aObj['fields']['friendlyname']}' <{$aObj['fields']['email']}> ({$aPlaceholders['$contact_to_notify$']}).");
- break;
- }
- }
- } else {
- Utils::Log(LOG_ERR, "Unable to find the contact with email = '$sEmailToNotify'. No contact to notify will be defined.");
- }
- }
- $sSynchroUser = Utils::GetConfigurationValue('synchro_user') ?: Utils::GetConfigurationValue('itop_login');
- $aPlaceholders['$synchro_user$'] = 0;
- if ($sSynchroUser != '') {
- $oRestClient = new RestClient();
- $aRes = $oRestClient->Get('User', array('login' => $sSynchroUser));
- if ($aRes['code'] == 0) {
- foreach ($aRes['objects'] as $sKey => $aObj) {
- if (!array_key_exists('key', $aObj)) {
- // Emulate the behavior for older versions of the API
- if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
- $aPlaceholders['$synchro_user$'] = (int)$aMatches[1];
- }
- } else {
- $aPlaceholders['$synchro_user$'] = (int)$aObj['key'];
- }
- Utils::Log(LOG_INFO, "Synchro User: '{$aObj['fields']['friendlyname']}' <{$aObj['fields']['email']}> ({$aPlaceholders['$synchro_user$']}).");
- break;
- }
- } else {
- if (array_key_exists('message', $aRes)) {
- Utils::Log(LOG_ERR, "Unable to use synchro user with login = '$sSynchroUser'. ".$aRes['message']);
- } else {
- Utils::Log(LOG_ERR, "Unable to use synchro user with login = '$sSynchroUser'. No user is defined.");
- }
- }
- }
- $aOtherPlaceholders = Utils::GetConfigurationValue('json_placeholders', array());
+ /**
+ * Initializes the synchronization data sources in iTop, according to the collectors' JSON specifications
+ *
+ * @param string[] $aCollectors list of classes implementing {@link Collector}
+ *
+ * @return boolean True if Ok, false otherwise
+ * @throws \InvalidConfigException
+ */
+ public function InitSynchroDataSources($aCollectors)
+ {
+ $bResult = true;
+ $aPlaceholders = [];
+ $sEmailToNotify = Utils::GetConfigurationValue('contact_to_notify', '');
+ $aPlaceholders['$contact_to_notify$'] = 0;
+ if ($sEmailToNotify != '') {
+ $oRestClient = new RestClient();
+ $aRes = $oRestClient->Get('Contact', ['email' => $sEmailToNotify]);
+ if ($aRes['code'] == 0) {
+ if (!is_array($aRes['objects'])) {
+ Utils::Log(LOG_WARNING, "Contact to notify ($sEmailToNotify) not found in iTop. Nobody will be notified of the results of the synchronization.");
+ } else {
+ foreach ($aRes['objects'] as $sKey => $aObj) {
+ if (!array_key_exists('key', $aObj)) {
+ // Emulate the behavior for older versions of the API
+ if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
+ $aPlaceholders['$contact_to_notify$'] = (int)$aMatches[1];
+ }
+ } else {
+ $aPlaceholders['$contact_to_notify$'] = (int)$aObj['key'];
+ }
+ Utils::Log(LOG_INFO, "Contact to notify: '{$aObj['fields']['friendlyname']}' <{$aObj['fields']['email']}> ({$aPlaceholders['$contact_to_notify$']}).");
+ break;
+ }
+ }
+ } else {
+ Utils::Log(LOG_ERR, "Unable to find the contact with email = '$sEmailToNotify'. No contact to notify will be defined.");
+ }
+ }
+ $sSynchroUser = Utils::GetConfigurationValue('synchro_user') ?: Utils::GetConfigurationValue('itop_login');
+ $aPlaceholders['$synchro_user$'] = 0;
+ if ($sSynchroUser != '') {
+ $oRestClient = new RestClient();
+ $aRes = $oRestClient->Get('User', ['login' => $sSynchroUser]);
+ if ($aRes['code'] == 0) {
+ foreach ($aRes['objects'] as $sKey => $aObj) {
+ if (!array_key_exists('key', $aObj)) {
+ // Emulate the behavior for older versions of the API
+ if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
+ $aPlaceholders['$synchro_user$'] = (int)$aMatches[1];
+ }
+ } else {
+ $aPlaceholders['$synchro_user$'] = (int)$aObj['key'];
+ }
+ Utils::Log(LOG_INFO, "Synchro User: '{$aObj['fields']['friendlyname']}' <{$aObj['fields']['email']}> ({$aPlaceholders['$synchro_user$']}).");
+ break;
+ }
+ } else {
+ if (array_key_exists('message', $aRes)) {
+ Utils::Log(LOG_ERR, "Unable to use synchro user with login = '$sSynchroUser'. ".$aRes['message']);
+ } else {
+ Utils::Log(LOG_ERR, "Unable to use synchro user with login = '$sSynchroUser'. No user is defined.");
+ }
+ }
+ }
+ $aOtherPlaceholders = Utils::GetConfigurationValue('json_placeholders', []);
- if (is_array($aOtherPlaceholders)) {
- foreach ($aOtherPlaceholders as $sKey => $sValue) {
- $aPlaceholders['$'.$sKey.'$'] = $sValue;
- }
- }
+ if (is_array($aOtherPlaceholders)) {
+ foreach ($aOtherPlaceholders as $sKey => $sValue) {
+ $aPlaceholders['$'.$sKey.'$'] = $sValue;
+ }
+ }
- /** @var \Collector $oCollector */
- foreach ($aCollectors as $oCollector) {
- Utils::SetCollector($oCollector, "InitSynchroDataSource");
- $bResult = $oCollector->InitSynchroDataSource($aPlaceholders);
- if (!$bResult) {
- break;
- }
- }
- Utils::SetCollector(null);
+ /** @var \Collector $oCollector */
+ foreach ($aCollectors as $oCollector) {
+ Utils::SetCollector($oCollector, "InitSynchroDataSource");
+ $bResult = $oCollector->InitSynchroDataSource($aPlaceholders);
+ if (!$bResult) {
+ break;
+ }
+ }
+ Utils::SetCollector(null);
- return $bResult;
- }
+ return $bResult;
+ }
- /**
- * Run the first pass of data collection: fetching the raw data from inventory scripts
- *
- * @param string[] $aCollectors list of classes implementing {@link Collector}
- * @param int $iMaxChunkSize
- * @param boolean $bCollectOnly
- *
- * @return boolean True if Ok, false otherwise
- */
- public function Collect($aCollectors, $iMaxChunkSize, $bCollectOnly)
- {
- $bResult = true;
- /** @var \Collector $oCollector */
- foreach ($aCollectors as $oCollector) {
- Utils::SetCollector($oCollector, "Collect");
- $bResult = $oCollector->Collect($iMaxChunkSize, $bCollectOnly);
- if (!$bResult) {
- break;
- }
- }
- Utils::SetCollector(null);
+ /**
+ * Run the first pass of data collection: fetching the raw data from inventory scripts
+ *
+ * @param string[] $aCollectors list of classes implementing {@link Collector}
+ * @param int $iMaxChunkSize
+ * @param boolean $bCollectOnly
+ *
+ * @return boolean True if Ok, false otherwise
+ */
+ public function Collect($aCollectors, $iMaxChunkSize, $bCollectOnly)
+ {
+ $bResult = true;
+ /** @var \Collector $oCollector */
+ foreach ($aCollectors as $oCollector) {
+ Utils::SetCollector($oCollector, "Collect");
+ $bResult = $oCollector->Collect($iMaxChunkSize, $bCollectOnly);
+ if (!$bResult) {
+ break;
+ }
+ }
+ Utils::SetCollector(null);
- return $bResult;
- }
+ return $bResult;
+ }
- /**
- * Run the final pass of the collection: synchronizing the data into iTop
- *
- * @param string[] $aCollectors list of classes implementing {@link Collector}
- *
- * @return boolean
- */
- public function Synchronize($aCollectors)
- {
- $bResult = true;
- $sStopOnError = Utils::GetConfigurationValue('stop_on_synchro_error', 'no');
- if (($sStopOnError != 'yes') && ($sStopOnError != 'no')) {
- Utils::Log(LOG_WARNING, "Unexpected value '$sStopOnError' for the parameter 'stop_on_synchro_error'. Will NOT stop on error. The expected values for this parameter are 'yes' or 'no'.");
- }
- $bStopOnError = ($sStopOnError == 'yes');
- /** @var \Collector $oCollector */
- foreach ($aCollectors as $oCollector) {
- Utils::SetCollector($oCollector, "Synchronize");
- $bResult = $oCollector->Synchronize();
- if (!$bResult) {
- if ($bStopOnError) {
- break;
- } else {
- // Do not report the error (it impacts the return code of the process)
- $bResult = true;
- }
- }
- }
- Utils::SetCollector(null);
+ /**
+ * Run the final pass of the collection: synchronizing the data into iTop
+ *
+ * @param string[] $aCollectors list of classes implementing {@link Collector}
+ *
+ * @return boolean
+ */
+ public function Synchronize($aCollectors)
+ {
+ $bResult = true;
+ $sStopOnError = Utils::GetConfigurationValue('stop_on_synchro_error', 'no');
+ if (($sStopOnError != 'yes') && ($sStopOnError != 'no')) {
+ Utils::Log(LOG_WARNING, "Unexpected value '$sStopOnError' for the parameter 'stop_on_synchro_error'. Will NOT stop on error. The expected values for this parameter are 'yes' or 'no'.");
+ }
+ $bStopOnError = ($sStopOnError == 'yes');
+ /** @var \Collector $oCollector */
+ foreach ($aCollectors as $oCollector) {
+ Utils::SetCollector($oCollector, "Synchronize");
+ $bResult = $oCollector->Synchronize();
+ if (!$bResult) {
+ if ($bStopOnError) {
+ break;
+ } else {
+ // Do not report the error (it impacts the return code of the process)
+ $bResult = true;
+ }
+ }
+ }
+ Utils::SetCollector(null);
- return $bResult;
- }
+ return $bResult;
+ }
- /////////////////////////////////////////////////////////////////////////
- //
- // Internal methods
- //
- /////////////////////////////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////
+ //
+ // Internal methods
+ //
+ /////////////////////////////////////////////////////////////////////////
- /**
- * Helper callback for sorting the collectors using the built-in uasort function
- *
- * @param array $aCollector1
- * @param array $aCollector2
- *
- * @return number
- */
- static public function CompareCollectors($aCollector1, $aCollector2)
- {
- if ($aCollector1['order'] == $aCollector2['order']) {
- return 0;
- }
+ /**
+ * Helper callback for sorting the collectors using the built-in uasort function
+ *
+ * @param array $aCollector1
+ * @param array $aCollector2
+ *
+ * @return number
+ */
+ public static function CompareCollectors($aCollector1, $aCollector2)
+ {
+ if ($aCollector1['order'] == $aCollector2['order']) {
+ return 0;
+ }
- return ($aCollector1['order'] > $aCollector2['order']) ? +1 : -1;
- }
+ return ($aCollector1['order'] > $aCollector2['order']) ? +1 : -1;
+ }
}
diff --git a/core/parameters.class.inc.php b/core/parameters.class.inc.php
index c780452..c63df34 100644
--- a/core/parameters.class.inc.php
+++ b/core/parameters.class.inc.php
@@ -1,4 +1,5 @@
aData = array();
- if ($sInputFile != null) {
- $this->LoadFromFile($sInputFile);
- }
- }
-
- public function Get($sCode, $default = '')
- {
- if (array_key_exists($sCode, $this->aData)) {
- return $this->aData[$sCode];
- }
-
- return $default;
- }
-
- public function Set($sCode, $value)
- {
- $this->aData[$sCode] = $value;
- }
-
- protected function ToXML(DOMNode $oRoot, $data = null, $sNodeName = null)
- {
- if ($data === null) {
- $data = $this->aData;
- }
-
- if ($oRoot instanceof DOMDocument) {
- $oNode = $oRoot->createElement($sNodeName);
- } else {
- $oNode = $oRoot->ownerDocument->createElement($sNodeName);
- }
- $oRoot->appendChild($oNode);
-
- if (is_array($data)) {
-
- $aKeys = array_keys($data);
- $bNumericKeys = true;
- foreach ($aKeys as $idx => $subkey) {
- if (((int)$subkey) !== $subkey) {
- $bNumericKeys = false;
- break;
- }
- }
- if ($bNumericKeys) {
- $oNode->setAttribute("type", "array");
- foreach ($data as $key => $value) {
- $this->ToXML($oNode, $value, 'item');
- }
- } else {
- foreach ($data as $key => $value) {
- $this->ToXML($oNode, $value, $key);
- }
- }
- } else {
- $oTextNode = $oRoot->ownerDocument->createTextNode($data);
- $oNode->appendChild($oTextNode);
- }
-
- return $oNode;
- }
-
- public function SaveToFile($sFileName)
- {
- $oDoc = new DOMDocument('1.0', 'UTF-8');
- $oDoc->preserveWhiteSpace = false;
- $oDoc->formatOutput = true;
- $this->ToXML($oDoc, null, 'parameters');
- $oDoc->save($sFileName);
- }
-
- public function Dump()
- {
- $oDoc = new DOMDocument('1.0', 'UTF-8');
- $oDoc->preserveWhiteSpace = false;
- $oDoc->formatOutput = true;
- $this->ToXML($oDoc, null, 'parameters');
-
- return $oDoc->saveXML();
- }
-
- public function LoadFromFile($sParametersFile)
- {
- $this->sParametersFile = $sParametersFile;
- if ($this->aData == null) {
- libxml_use_internal_errors(true);
- $oXML = @simplexml_load_file($this->sParametersFile);
- if (!$oXML) {
- $aMessage = array();
- foreach (libxml_get_errors() as $oError) {
- $aMessage[] = "(line: {$oError->line}) ".$oError->message; // Beware: $oError->columns sometimes returns wrong (misleading) value
- }
- libxml_clear_errors();
- throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': ".implode(' ', $aMessage));
- }
-
- $this->aData = array();
- foreach ($oXML as $key => $oElement) {
- $this->aData[(string)$key] = $this->ReadElement($oElement);
- }
- }
- }
-
- protected function ReadElement(SimpleXMLElement $oElement)
- {
- $sDefaultNodeType = (count($oElement->children()) > 0) ? 'hash' : 'string';
- $sNodeType = $this->GetAttribute('type', $oElement, $sDefaultNodeType);
- switch ($sNodeType) {
- case 'array':
- $value = array();
- // Treat the current element as zero based array, child tag names are NOT meaningful
- $sFirstTagName = null;
- foreach ($oElement->children() as $oChildElement) {
- if ($sFirstTagName == null) {
- $sFirstTagName = $oChildElement->getName();
- } else if ($sFirstTagName != $oChildElement->getName()) {
- throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': mixed tags ('$sFirstTagName' and '".$oChildElement->getName()."') inside array '".$oElement->getName()."'");
- }
- $val = $this->ReadElement($oChildElement);
- $value[] = $val;
- }
- break;
-
- case 'hash':
- $value = array();
- // Treat the current element as a hash, child tag names are keys
- foreach ($oElement->children() as $oChildElement) {
- if (array_key_exists($oChildElement->getName(), $value)) {
- throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': duplicate tags '".$oChildElement->getName()."' inside hash '".$oElement->getName()."'");
- }
- $val = $this->ReadElement($oChildElement);
- $value[$oChildElement->getName()] = $val;
- }
- break;
-
- case 'int':
- case 'integer':
- $value = (int)$oElement;
- break;
-
- case 'string':
- default:
- $value = (string)$oElement;
- }
-
- return $value;
- }
-
- protected function GetAttribute($sAttName, $oElement, $sDefaultValue)
- {
- $sRet = $sDefaultValue;
-
- foreach ($oElement->attributes() as $sKey => $oChildElement) {
- if ((string)$sKey == $sAttName) {
- $sRet = (string)$oChildElement;
- break;
- }
- }
-
- return $sRet;
- }
-
- function Merge(Parameters $oTask)
- {
- $this->aData = $this->array_merge_recursive_distinct($this->aData, $oTask->aData);
- }
-
- /**
- * array_merge_recursive does indeed merge arrays, but it converts values with duplicate
- * keys to arrays rather than overwriting the value in the first array with the duplicate
- * value in the second array, as array_merge does. I.e., with array_merge_recursive,
- * this happens (documented behavior):
- *
- * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
- * => array('key' => array('org value', 'new value'));
- *
- * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
- * Matching keys' values in the second array overwrite those in the first array, as is the
- * case with array_merge, i.e.:
- *
- * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
- * => array('key' => array('new value'));
- *
- * Parameters are passed by reference, though only for performance reasons. They're not
- * altered by this function.
- *
- * @param array $array1
- * @param array $array2
- *
- * @return array
- * @author Daniel
- * @author Gabriel Sobrinho
- */
- protected function array_merge_recursive_distinct(array &$array1, array &$array2)
- {
- $merged = $array1;
-
- foreach ($array2 as $key => &$value) {
- if (is_array($value) && isset ($merged [$key]) && is_array($merged [$key])) {
- $merged [$key] = $this->array_merge_recursive_distinct($merged [$key], $value);
- } else {
- $merged [$key] = $value;
- }
- }
-
- return $merged;
- }
+ protected $aData = null;
+ protected $sParametersFile;
+
+ public function __construct($sInputFile = null)
+ {
+ $this->aData = [];
+ if ($sInputFile != null) {
+ $this->LoadFromFile($sInputFile);
+ }
+ }
+
+ public function Get($sCode, $default = '')
+ {
+ if (array_key_exists($sCode, $this->aData)) {
+ return $this->aData[$sCode];
+ }
+
+ return $default;
+ }
+
+ public function Set($sCode, $value)
+ {
+ $this->aData[$sCode] = $value;
+ }
+
+ protected function ToXML(DOMNode $oRoot, $data = null, $sNodeName = null)
+ {
+ if ($data === null) {
+ $data = $this->aData;
+ }
+
+ if ($oRoot instanceof DOMDocument) {
+ $oNode = $oRoot->createElement($sNodeName);
+ } else {
+ $oNode = $oRoot->ownerDocument->createElement($sNodeName);
+ }
+ $oRoot->appendChild($oNode);
+
+ if (is_array($data)) {
+
+ $aKeys = array_keys($data);
+ $bNumericKeys = true;
+ foreach ($aKeys as $idx => $subkey) {
+ if (((int)$subkey) !== $subkey) {
+ $bNumericKeys = false;
+ break;
+ }
+ }
+ if ($bNumericKeys) {
+ $oNode->setAttribute("type", "array");
+ foreach ($data as $key => $value) {
+ $this->ToXML($oNode, $value, 'item');
+ }
+ } else {
+ foreach ($data as $key => $value) {
+ $this->ToXML($oNode, $value, $key);
+ }
+ }
+ } else {
+ $oTextNode = $oRoot->ownerDocument->createTextNode($data);
+ $oNode->appendChild($oTextNode);
+ }
+
+ return $oNode;
+ }
+
+ public function SaveToFile($sFileName)
+ {
+ $oDoc = new DOMDocument('1.0', 'UTF-8');
+ $oDoc->preserveWhiteSpace = false;
+ $oDoc->formatOutput = true;
+ $this->ToXML($oDoc, null, 'parameters');
+ $oDoc->save($sFileName);
+ }
+
+ public function Dump()
+ {
+ $oDoc = new DOMDocument('1.0', 'UTF-8');
+ $oDoc->preserveWhiteSpace = false;
+ $oDoc->formatOutput = true;
+ $this->ToXML($oDoc, null, 'parameters');
+
+ return $oDoc->saveXML();
+ }
+
+ public function LoadFromFile($sParametersFile)
+ {
+ $this->sParametersFile = $sParametersFile;
+ if ($this->aData == null) {
+ libxml_use_internal_errors(true);
+ $oXML = @simplexml_load_file($this->sParametersFile);
+ if (!$oXML) {
+ $aMessage = [];
+ foreach (libxml_get_errors() as $oError) {
+ $aMessage[] = "(line: {$oError->line}) ".$oError->message; // Beware: $oError->columns sometimes returns wrong (misleading) value
+ }
+ libxml_clear_errors();
+ throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': ".implode(' ', $aMessage));
+ }
+
+ $this->aData = [];
+ foreach ($oXML as $key => $oElement) {
+ $this->aData[(string)$key] = $this->ReadElement($oElement);
+ }
+ }
+ }
+
+ protected function ReadElement(SimpleXMLElement $oElement)
+ {
+ $sDefaultNodeType = (count($oElement->children()) > 0) ? 'hash' : 'string';
+ $sNodeType = $this->GetAttribute('type', $oElement, $sDefaultNodeType);
+ switch ($sNodeType) {
+ case 'array':
+ $value = [];
+ // Treat the current element as zero based array, child tag names are NOT meaningful
+ $sFirstTagName = null;
+ foreach ($oElement->children() as $oChildElement) {
+ if ($sFirstTagName == null) {
+ $sFirstTagName = $oChildElement->getName();
+ } elseif ($sFirstTagName != $oChildElement->getName()) {
+ throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': mixed tags ('$sFirstTagName' and '".$oChildElement->getName()."') inside array '".$oElement->getName()."'");
+ }
+ $val = $this->ReadElement($oChildElement);
+ $value[] = $val;
+ }
+ break;
+
+ case 'hash':
+ $value = [];
+ // Treat the current element as a hash, child tag names are keys
+ foreach ($oElement->children() as $oChildElement) {
+ if (array_key_exists($oChildElement->getName(), $value)) {
+ throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': duplicate tags '".$oChildElement->getName()."' inside hash '".$oElement->getName()."'");
+ }
+ $val = $this->ReadElement($oChildElement);
+ $value[$oChildElement->getName()] = $val;
+ }
+ break;
+
+ case 'int':
+ case 'integer':
+ $value = (int)$oElement;
+ break;
+
+ case 'string':
+ default:
+ $value = (string)$oElement;
+ }
+
+ return $value;
+ }
+
+ protected function GetAttribute($sAttName, $oElement, $sDefaultValue)
+ {
+ $sRet = $sDefaultValue;
+
+ foreach ($oElement->attributes() as $sKey => $oChildElement) {
+ if ((string)$sKey == $sAttName) {
+ $sRet = (string)$oChildElement;
+ break;
+ }
+ }
+
+ return $sRet;
+ }
+
+ public function Merge(Parameters $oTask)
+ {
+ $this->aData = $this->array_merge_recursive_distinct($this->aData, $oTask->aData);
+ }
+
+ /**
+ * array_merge_recursive does indeed merge arrays, but it converts values with duplicate
+ * keys to arrays rather than overwriting the value in the first array with the duplicate
+ * value in the second array, as array_merge does. I.e., with array_merge_recursive,
+ * this happens (documented behavior):
+ *
+ * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
+ * => array('key' => array('org value', 'new value'));
+ *
+ * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
+ * Matching keys' values in the second array overwrite those in the first array, as is the
+ * case with array_merge, i.e.:
+ *
+ * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
+ * => array('key' => array('new value'));
+ *
+ * Parameters are passed by reference, though only for performance reasons. They're not
+ * altered by this function.
+ *
+ * @param array $array1
+ * @param array $array2
+ *
+ * @return array
+ * @author Daniel
+ * @author Gabriel Sobrinho
+ */
+ protected function array_merge_recursive_distinct(array &$array1, array &$array2)
+ {
+ $merged = $array1;
+
+ foreach ($array2 as $key => &$value) {
+ if (is_array($value) && isset($merged [$key]) && is_array($merged [$key])) {
+ $merged [$key] = $this->array_merge_recursive_distinct($merged [$key], $value);
+ } else {
+ $merged [$key] = $value;
+ }
+ }
+
+ return $merged;
+ }
}
diff --git a/core/polyfill.inc.php b/core/polyfill.inc.php
index c965118..dc82eee 100644
--- a/core/polyfill.inc.php
+++ b/core/polyfill.inc.php
@@ -5,15 +5,15 @@
* Make this function available even for (PHP 8 < 8.1.0)
*/
if (!function_exists("array_is_list")) {
- function array_is_list(array $array): bool {
- $i = 0;
- foreach ($array as $k => $v) {
- if ($k !== $i++) {
- return false;
- }
- }
+ function array_is_list(array $array): bool
+ {
+ $i = 0;
+ foreach ($array as $k => $v) {
+ if ($k !== $i++) {
+ return false;
+ }
+ }
- return true;
- }
+ return true;
+ }
}
-
diff --git a/core/restclient.class.inc.php b/core/restclient.class.inc.php
index e54cbf8..150841d 100644
--- a/core/restclient.class.inc.php
+++ b/core/restclient.class.inc.php
@@ -1,4 +1,5 @@
sVersion = '1.0';
- }
-
- public function GetVersion()
- {
- return $this->sVersion;
- }
-
- public function SetVersion($sVersion)
- {
- $this->sVersion = $sVersion;
- }
-
-
- public function Get($sClass, $keySpec, $sOutputFields = '*', $iLimit = 0)
- {
- $aOperation = array(
- 'operation' => 'core/get', // operation code
- 'class' => $sClass,
- 'key' => $keySpec,
- 'output_fields' => $sOutputFields, // list of fields to show in the results (* or a,b,c)
- 'limit' => $iLimit,
- );
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- public function CheckCredentials($sUser, $sPassword)
- {
- $aOperation = array(
- 'operation' => 'core/check_credentials', // operation code
- 'user' => $sUser,
- 'password' => $sPassword,
- );
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- public function ListOperations()
- {
- $aOperation = array(
- 'operation' => 'list_operations', // operation code
- 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
- );
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- public function Create($sClass, $aFields, $sComment)
- {
- $aOperation = array(
- 'operation' => 'core/create', // operation code
- 'class' => $sClass,
- 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
- 'fields' => $aFields,
- 'comment' => $sComment,
- );
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- public function Update($sClass, $keySpec, $aFields, $sComment)
- {
- $aOperation = array(
- 'operation' => 'core/update', // operation code
- 'class' => $sClass,
- 'key' => $keySpec,
- 'fields' => $aFields, // fields to update
- 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
- 'comment' => $sComment,
- );
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- public function GetRelatedObjects($sClass, $sKey, $sRelation, $bRedundancy = false, $iDepth = 99)
- {
- $aOperation = array(
- 'operation' => 'core/get_related', // operation code
- 'class' => $sClass,
- 'key' => $sKey,
- 'relation' => $sRelation,
- 'depth' => $iDepth,
- 'redundancy' => $bRedundancy,
- );
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- protected static function ExecOperation($aOperation, $sVersion = '1.0')
- {
- $aData = Utils::GetCredentials();
- $aData['json_data'] = json_encode($aOperation);
- $sLoginform = Utils::GetLoginMode();
- $sUrl = sprintf('%s/webservices/rest.php?login_mode=%s&version=%s',
- Utils::GetConfigurationValue('itop_url', ''),
- $sLoginform,
- $sVersion
- );
- $aHeaders = array();
- $aCurlOptions = Utils::GetCurlOptions();
- $response = Utils::DoPostRequest($sUrl, $aData, '', $aHeaders, $aCurlOptions);
- $aResults = json_decode($response, true);
- if (!$aResults) {
- throw new Exception("rest.php replied: $response");
- }
-
- return $aResults;
- }
-
- public static function GetNewestKnownVersion()
- {
- $sNewestVersion = '1.0';
- $oC = new RestClient();
- $aKnownVersions = array('1.0', '1.1', '1.2', '2.0');
- foreach ($aKnownVersions as $sVersion) {
- $oC->SetVersion($sVersion);
- $aRet = $oC->ListOperations();
- if ($aRet['code'] == 0) {
- // Supported version
- $sNewestVersion = $sVersion;
- }
- }
-
- return $sNewestVersion;
- }
-
- /**
- * Emulates the behavior of Get('*+') to retrieve all the characteristics
- * of the attribute_list of a given synchro data source
- *
- * @param hash $aSource The definition of 'fields' the Synchro DataSource, as retrieved by Get
- * @param integer $iSourceId The identifier (key) of the Synchro Data Source
- */
- public static function GetFullSynchroDataSource(&$aSource, $iSourceId)
- {
- $bResult = true;
- $aAttributes = array();
- // Optimize the calls to the REST API: one call per finalclass
- foreach ($aSource['attribute_list'] as $aAttr) {
- if (!array_key_exists($aAttr['finalclass'], $aAttributes)) {
- $aAttributes[$aAttr['finalclass']] = array();
- }
- $aAttributes[$aAttr['finalclass']][] = $aAttr['attcode'];
- }
-
- $oRestClient = new RestClient();
- foreach ($aAttributes as $sFinalClass => $aAttCodes) {
- Utils::Log(LOG_DEBUG, "RestClient::Get SELECT $sFinalClass WHERE attcode IN ('".implode("','", $aAttCodes)."') AND sync_source_id = $iSourceId");
- $aResult = $oRestClient->Get($sFinalClass, "SELECT $sFinalClass WHERE attcode IN ('".implode("','", $aAttCodes)."') AND sync_source_id = $iSourceId");
- if ($aResult['code'] != 0) {
- Utils::Log(LOG_ERR, "{$aResult['message']} ({$aResult['code']})");
- $bResult = false;
- } else {
- // Update the SDS Attributes
- foreach ($aSource['attribute_list'] as $idx => $aAttr) {
- foreach ($aResult['objects'] as $aAttDef) {
- if ($aAttDef['fields']['attcode'] == $aAttr['attcode']) {
- $aSource['attribute_list'][$idx] = $aAttDef['fields'];
-
- // fix booleans
- $aSource['attribute_list'][$idx]['reconcile'] = $aAttDef['fields']['reconcile'] ? '1' : '0';
- $aSource['attribute_list'][$idx]['update'] = $aAttDef['fields']['update'] ? '1' : '0';
-
- // read-only (external) fields
- unset($aSource['attribute_list'][$idx]['friendlyname']);
- unset($aSource['attribute_list'][$idx]['sync_source_id']);
- unset($aSource['attribute_list'][$idx]['sync_source_name']);
- unset($aSource['attribute_list'][$idx]['sync_source_id_friendlyname']);
- }
- }
- }
- }
- }
-
- // Don't care about these read-only fields
- unset($aSource['friendlyname']);
- unset($aSource['user_id_friendlyname']);
- unset($aSource['user_id_finalclass_recall']);
- unset($aSource['notify_contact_id_friendlyname']);
- unset($aSource['notify_contact_id_finalclass_recall']);
- unset($aSource['notify_contact_id_obsolescence_flag']);
-
- return $bResult;
- }
+ protected $sVersion;
+
+ public function __construct()
+ {
+ $this->sVersion = '1.0';
+ }
+
+ public function GetVersion()
+ {
+ return $this->sVersion;
+ }
+
+ public function SetVersion($sVersion)
+ {
+ $this->sVersion = $sVersion;
+ }
+
+ public function Get($sClass, $keySpec, $sOutputFields = '*', $iLimit = 0)
+ {
+ $aOperation = [
+ 'operation' => 'core/get', // operation code
+ 'class' => $sClass,
+ 'key' => $keySpec,
+ 'output_fields' => $sOutputFields, // list of fields to show in the results (* or a,b,c)
+ 'limit' => $iLimit,
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ public function CheckCredentials($sUser, $sPassword)
+ {
+ $aOperation = [
+ 'operation' => 'core/check_credentials', // operation code
+ 'user' => $sUser,
+ 'password' => $sPassword,
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ public function ListOperations()
+ {
+ $aOperation = [
+ 'operation' => 'list_operations', // operation code
+ 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ public function Create($sClass, $aFields, $sComment)
+ {
+ $aOperation = [
+ 'operation' => 'core/create', // operation code
+ 'class' => $sClass,
+ 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
+ 'fields' => $aFields,
+ 'comment' => $sComment,
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ public function Update($sClass, $keySpec, $aFields, $sComment)
+ {
+ $aOperation = [
+ 'operation' => 'core/update', // operation code
+ 'class' => $sClass,
+ 'key' => $keySpec,
+ 'fields' => $aFields, // fields to update
+ 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
+ 'comment' => $sComment,
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ public function GetRelatedObjects($sClass, $sKey, $sRelation, $bRedundancy = false, $iDepth = 99)
+ {
+ $aOperation = [
+ 'operation' => 'core/get_related', // operation code
+ 'class' => $sClass,
+ 'key' => $sKey,
+ 'relation' => $sRelation,
+ 'depth' => $iDepth,
+ 'redundancy' => $bRedundancy,
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ protected static function ExecOperation($aOperation, $sVersion = '1.0')
+ {
+ $aData = Utils::GetCredentials();
+ $aData['json_data'] = json_encode($aOperation);
+ $sLoginform = Utils::GetLoginMode();
+ $sUrl = sprintf(
+ '%s/webservices/rest.php?login_mode=%s&version=%s',
+ Utils::GetConfigurationValue('itop_url', ''),
+ $sLoginform,
+ $sVersion
+ );
+ $aHeaders = [];
+ $aCurlOptions = Utils::GetCurlOptions();
+ $response = Utils::DoPostRequest($sUrl, $aData, '', $aHeaders, $aCurlOptions);
+ $aResults = json_decode($response, true);
+ if (!$aResults) {
+ throw new Exception("rest.php replied: $response");
+ }
+
+ return $aResults;
+ }
+
+ public static function GetNewestKnownVersion()
+ {
+ $sNewestVersion = '1.0';
+ $oC = new RestClient();
+ $aKnownVersions = ['1.0', '1.1', '1.2', '2.0'];
+ foreach ($aKnownVersions as $sVersion) {
+ $oC->SetVersion($sVersion);
+ $aRet = $oC->ListOperations();
+ if ($aRet['code'] == 0) {
+ // Supported version
+ $sNewestVersion = $sVersion;
+ }
+ }
+
+ return $sNewestVersion;
+ }
+
+ /**
+ * Emulates the behavior of Get('*+') to retrieve all the characteristics
+ * of the attribute_list of a given synchro data source
+ *
+ * @param hash $aSource The definition of 'fields' the Synchro DataSource, as retrieved by Get
+ * @param integer $iSourceId The identifier (key) of the Synchro Data Source
+ */
+ public static function GetFullSynchroDataSource(&$aSource, $iSourceId)
+ {
+ $bResult = true;
+ $aAttributes = [];
+ // Optimize the calls to the REST API: one call per finalclass
+ foreach ($aSource['attribute_list'] as $aAttr) {
+ if (!array_key_exists($aAttr['finalclass'], $aAttributes)) {
+ $aAttributes[$aAttr['finalclass']] = [];
+ }
+ $aAttributes[$aAttr['finalclass']][] = $aAttr['attcode'];
+ }
+
+ $oRestClient = new RestClient();
+ foreach ($aAttributes as $sFinalClass => $aAttCodes) {
+ Utils::Log(LOG_DEBUG, "RestClient::Get SELECT $sFinalClass WHERE attcode IN ('".implode("','", $aAttCodes)."') AND sync_source_id = $iSourceId");
+ $aResult = $oRestClient->Get($sFinalClass, "SELECT $sFinalClass WHERE attcode IN ('".implode("','", $aAttCodes)."') AND sync_source_id = $iSourceId");
+ if ($aResult['code'] != 0) {
+ Utils::Log(LOG_ERR, "{$aResult['message']} ({$aResult['code']})");
+ $bResult = false;
+ } else {
+ // Update the SDS Attributes
+ foreach ($aSource['attribute_list'] as $idx => $aAttr) {
+ foreach ($aResult['objects'] as $aAttDef) {
+ if ($aAttDef['fields']['attcode'] == $aAttr['attcode']) {
+ $aSource['attribute_list'][$idx] = $aAttDef['fields'];
+
+ // fix booleans
+ $aSource['attribute_list'][$idx]['reconcile'] = $aAttDef['fields']['reconcile'] ? '1' : '0';
+ $aSource['attribute_list'][$idx]['update'] = $aAttDef['fields']['update'] ? '1' : '0';
+
+ // read-only (external) fields
+ unset($aSource['attribute_list'][$idx]['friendlyname']);
+ unset($aSource['attribute_list'][$idx]['sync_source_id']);
+ unset($aSource['attribute_list'][$idx]['sync_source_name']);
+ unset($aSource['attribute_list'][$idx]['sync_source_id_friendlyname']);
+ }
+ }
+ }
+ }
+ }
+
+ // Don't care about these read-only fields
+ unset($aSource['friendlyname']);
+ unset($aSource['user_id_friendlyname']);
+ unset($aSource['user_id_finalclass_recall']);
+ unset($aSource['notify_contact_id_friendlyname']);
+ unset($aSource['notify_contact_id_finalclass_recall']);
+ unset($aSource['notify_contact_id_obsolescence_flag']);
+
+ return $bResult;
+ }
}
diff --git a/core/sqlcollector.class.inc.php b/core/sqlcollector.class.inc.php
index 187816b..5470be2 100644
--- a/core/sqlcollector.class.inc.php
+++ b/core/sqlcollector.class.inc.php
@@ -1,7 +1,8 @@
oDB = null;
- $this->oStatement = null;
- }
-
- /**
- * Runs the configured query to start fetching the data from the database
- *
- * @see Collector::Prepare()
- */
- public function Prepare()
- {
- $bRet = parent::Prepare();
- if (!$bRet) {
- return false;
- }
-
- $bRet = $this->Connect(); // Establish the connection to the database
- if (!$bRet) {
- return false;
- }
-
- // Read the SQL query from the configuration
- $this->sQuery = Utils::GetConfigurationValue(get_class($this)."_query", '');
- if ($this->sQuery == '') {
- // Try all lowercase
- $this->sQuery = Utils::GetConfigurationValue(strtolower(get_class($this))."_query", '');
- }
- if ($this->sQuery == '') {
- // No query at all !!
- Utils::Log(LOG_ERR, "[".get_class($this)."] no SQL query configured! Cannot collect data. The query was expected to be configured as '".strtolower(get_class($this))."_query' in the configuration file.");
-
- return false;
- }
-
-
- $this->oStatement = $this->oDB->prepare($this->sQuery);
- if ($this->oStatement === false) {
- $aInfo = $this->oDB->errorInfo();
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to prepare the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
-
- return false;
- }
-
- $this->oStatement->execute();
- if ($this->oStatement->errorCode() !== '00000') {
- $aInfo = $this->oStatement->errorInfo();
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to execute the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
-
- return false;
- }
-
- $this->idx = 0;
-
- return true;
- }
-
- /**
- * Establish the connection to the database, based on the configuration parameters.
- * By default all collectors derived from SQLCollector will share the same connection
- * parameters (same DB server, login, DB name...). If you don't want this behavior,
- * overload this method in your connector.
- */
- protected function Connect()
- {
- $aAvailableDrivers = PDO::getAvailableDrivers();
-
- Utils::Log(LOG_DEBUG, "Available PDO drivers: ".implode(', ', $aAvailableDrivers));
-
- $sEngine = Utils::GetConfigurationValue('sql_engine', 'mysql');
- if (!in_array($sEngine, $aAvailableDrivers)) {
- Utils::Log(LOG_ERR, "The requested PDO driver: '$sEngine' is not installed on this system. Available PDO drivers: ".implode(', ', $aAvailableDrivers));
- }
- $sHost = Utils::GetConfigurationValue('sql_host', 'localhost');
- $sDatabase = Utils::GetConfigurationValue('sql_database', '');
- $sLogin = Utils::GetConfigurationValue('sql_login', 'root');
- $sPassword = Utils::GetConfigurationValue('sql_password', '');
-
- $sConnectionStringFormat = Utils::GetConfigurationValue('sql_connection_string', '%1$s:dbname=%2$s;host=%3$s');
- $sConnectionString = sprintf($sConnectionStringFormat, $sEngine, $sDatabase, $sHost);
-
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Connection string: '$sConnectionString'");
-
- try {
- $this->oDB = new PDO($sConnectionString, $sLogin, $sPassword);
- }
- catch (PDOException $e) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] Database connection failed: ".$e->getMessage());
- $this->oDB = null;
-
- return false;
- }
-
- return true;
- }
-
- /**
- * Fetch one row of data from the database
- * The first row is used to check if the columns of the result match the expected "fields"
- *
- * @see Collector::Fetch()
- */
- public function Fetch()
- {
- if ($aData = $this->oStatement->fetch(PDO::FETCH_ASSOC)) {
-
- foreach ($this->aSkippedAttributes as $sCode) {
- unset($aData[$sCode]);
- }
-
- if ($this->idx == 0) {
- $this->CheckColumns($aData, [], 'SQL query');
- }
- $this->idx++;
-
- return $aData;
- }
-
- return false;
- }
-
- /**
- * Determine if a given attribute is allowed to be missing in the data datamodel.
- *
- * The implementation is based on a predefined configuration parameter named from the
- * class of the collector (all lowercase) with _ignored_attributes appended.
- *
- * Example: here is the configuration to "ignore" the attribute 'location_id' for the class MySQLCollector:
- *
- * location_id
- *
- *
- * @param string $sAttCode
- *
- * @return boolean True if the attribute can be skipped, false otherwise
- */
- public function AttributeIsOptional($sAttCode)
- {
- $aIgnoredAttributes = Utils::GetConfigurationValue(get_class($this)."_ignored_attributes", null);
- if ($aIgnoredAttributes === null) {
- // Try all lowercase
- $aIgnoredAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_ignored_attributes", null);
- }
- if (is_array($aIgnoredAttributes)) {
- if (in_array($sAttCode, $aIgnoredAttributes)) {
- return true;
- }
- }
-
- return parent::AttributeIsOptional($sAttCode);
- }
+ protected $oDB;
+ protected $oStatement;
+ protected $idx;
+ protected $sQuery;
+
+ /**
+ * Initalization
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ $this->oDB = null;
+ $this->oStatement = null;
+ }
+
+ /**
+ * Runs the configured query to start fetching the data from the database
+ *
+ * @see Collector::Prepare()
+ */
+ public function Prepare()
+ {
+ $bRet = parent::Prepare();
+ if (!$bRet) {
+ return false;
+ }
+
+ $bRet = $this->Connect(); // Establish the connection to the database
+ if (!$bRet) {
+ return false;
+ }
+
+ // Read the SQL query from the configuration
+ $this->sQuery = Utils::GetConfigurationValue(get_class($this)."_query", '');
+ if ($this->sQuery == '') {
+ // Try all lowercase
+ $this->sQuery = Utils::GetConfigurationValue(strtolower(get_class($this))."_query", '');
+ }
+ if ($this->sQuery == '') {
+ // No query at all !!
+ Utils::Log(LOG_ERR, "[".get_class($this)."] no SQL query configured! Cannot collect data. The query was expected to be configured as '".strtolower(get_class($this))."_query' in the configuration file.");
+
+ return false;
+ }
+
+ $this->oStatement = $this->oDB->prepare($this->sQuery);
+ if ($this->oStatement === false) {
+ $aInfo = $this->oDB->errorInfo();
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to prepare the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
+
+ return false;
+ }
+
+ $this->oStatement->execute();
+ if ($this->oStatement->errorCode() !== '00000') {
+ $aInfo = $this->oStatement->errorInfo();
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to execute the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
+
+ return false;
+ }
+
+ $this->idx = 0;
+
+ return true;
+ }
+
+ /**
+ * Establish the connection to the database, based on the configuration parameters.
+ * By default all collectors derived from SQLCollector will share the same connection
+ * parameters (same DB server, login, DB name...). If you don't want this behavior,
+ * overload this method in your connector.
+ */
+ protected function Connect()
+ {
+ $aAvailableDrivers = PDO::getAvailableDrivers();
+
+ Utils::Log(LOG_DEBUG, "Available PDO drivers: ".implode(', ', $aAvailableDrivers));
+
+ $sEngine = Utils::GetConfigurationValue('sql_engine', 'mysql');
+ if (!in_array($sEngine, $aAvailableDrivers)) {
+ Utils::Log(LOG_ERR, "The requested PDO driver: '$sEngine' is not installed on this system. Available PDO drivers: ".implode(', ', $aAvailableDrivers));
+ }
+ $sHost = Utils::GetConfigurationValue('sql_host', 'localhost');
+ $sDatabase = Utils::GetConfigurationValue('sql_database', '');
+ $sLogin = Utils::GetConfigurationValue('sql_login', 'root');
+ $sPassword = Utils::GetConfigurationValue('sql_password', '');
+
+ $sConnectionStringFormat = Utils::GetConfigurationValue('sql_connection_string', '%1$s:dbname=%2$s;host=%3$s');
+ $sConnectionString = sprintf($sConnectionStringFormat, $sEngine, $sDatabase, $sHost);
+
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Connection string: '$sConnectionString'");
+
+ try {
+ $this->oDB = new PDO($sConnectionString, $sLogin, $sPassword);
+ } catch (PDOException $e) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Database connection failed: ".$e->getMessage());
+ $this->oDB = null;
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Fetch one row of data from the database
+ * The first row is used to check if the columns of the result match the expected "fields"
+ *
+ * @see Collector::Fetch()
+ */
+ public function Fetch()
+ {
+ if ($aData = $this->oStatement->fetch(PDO::FETCH_ASSOC)) {
+
+ foreach ($this->aSkippedAttributes as $sCode) {
+ unset($aData[$sCode]);
+ }
+
+ if ($this->idx == 0) {
+ $this->CheckColumns($aData, [], 'SQL query');
+ }
+ $this->idx++;
+
+ return $aData;
+ }
+
+ return false;
+ }
+
+ /**
+ * Determine if a given attribute is allowed to be missing in the data datamodel.
+ *
+ * The implementation is based on a predefined configuration parameter named from the
+ * class of the collector (all lowercase) with _ignored_attributes appended.
+ *
+ * Example: here is the configuration to "ignore" the attribute 'location_id' for the class MySQLCollector:
+ *
+ * location_id
+ *
+ *
+ * @param string $sAttCode
+ *
+ * @return boolean True if the attribute can be skipped, false otherwise
+ */
+ public function AttributeIsOptional($sAttCode)
+ {
+ $aIgnoredAttributes = Utils::GetConfigurationValue(get_class($this)."_ignored_attributes", null);
+ if ($aIgnoredAttributes === null) {
+ // Try all lowercase
+ $aIgnoredAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_ignored_attributes", null);
+ }
+ if (is_array($aIgnoredAttributes)) {
+ if (in_array($sAttCode, $aIgnoredAttributes)) {
+ return true;
+ }
+ }
+
+ return parent::AttributeIsOptional($sAttCode);
+ }
}
/**
@@ -209,42 +208,41 @@ public function AttributeIsOptional($sAttCode)
*/
abstract class MySQLCollector extends SQLCollector
{
- /**
- * Establish the connection to the database, based on the configuration parameters.
- * By default all collectors derived from SQLCollector will share the same connection
- * parameters (same DB server, login, DB name...).
- * Moreover, forces the connection to use utf8 using the SET NAMES SQL command.
- * If you don't want this behavior, overload this method in your connector.
- */
- protected function Connect()
- {
- $bRet = parent::Connect();
- if ($bRet) {
- try {
- $this->oStatement = $this->oDB->prepare("SET NAMES 'utf8'");
- if ($this->oStatement === false) {
- $aInfo = $this->oDB->errorInfo();
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to prepare the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
-
- return false;
- }
-
- $bRet = $this->oStatement->execute();
- if ($this->oStatement->errorCode() !== '00000') {
- $aInfo = $this->oStatement->errorInfo();
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to execute the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
-
- return false;
- }
- }
- catch (PDOException $e) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] SQL query: \"SET NAMES 'utf8'\" failed: ".$e->getMessage());
- $this->oDB = null;
-
- return false;
- }
- }
-
- return $bRet;
- }
+ /**
+ * Establish the connection to the database, based on the configuration parameters.
+ * By default all collectors derived from SQLCollector will share the same connection
+ * parameters (same DB server, login, DB name...).
+ * Moreover, forces the connection to use utf8 using the SET NAMES SQL command.
+ * If you don't want this behavior, overload this method in your connector.
+ */
+ protected function Connect()
+ {
+ $bRet = parent::Connect();
+ if ($bRet) {
+ try {
+ $this->oStatement = $this->oDB->prepare("SET NAMES 'utf8'");
+ if ($this->oStatement === false) {
+ $aInfo = $this->oDB->errorInfo();
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to prepare the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
+
+ return false;
+ }
+
+ $bRet = $this->oStatement->execute();
+ if ($this->oStatement->errorCode() !== '00000') {
+ $aInfo = $this->oStatement->errorInfo();
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to execute the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
+
+ return false;
+ }
+ } catch (PDOException $e) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] SQL query: \"SET NAMES 'utf8'\" failed: ".$e->getMessage());
+ $this->oDB = null;
+
+ return false;
+ }
+ }
+
+ return $bRet;
+ }
}
diff --git a/core/utils.class.inc.php b/core/utils.class.inc.php
index 5107ba9..1b066d0 100644
--- a/core/utils.class.inc.php
+++ b/core/utils.class.inc.php
@@ -1,4 +1,5 @@
$sArg) {
- if (preg_match('/^--'.$sParamName.'=(.*)$/', $sArg, $aMatches)) {
- $retValue = $aMatches[1];
- }
- }
- }
-
- return $retValue;
- }
-
- static public function ReadBooleanParameter($sParamName, $defaultValue)
- {
- global $argv;
-
- $retValue = $defaultValue;
- if (is_array($argv)) {
- foreach ($argv as $iArg => $sArg) {
- if (preg_match('/^--'.$sParamName.'$/', $sArg, $aMatches)) {
- $retValue = true;
- } else if (preg_match('/^--'.$sParamName.'=(.*)$/', $sArg, $aMatches)) {
- $retValue = ($aMatches[1] != 0);
- }
- }
- }
-
- return $retValue;
- }
-
- static public function CheckParameters($aOptionalParams)
- {
- global $argv;
-
- $aUnknownParams = array();
- if (is_array($argv)) {
- foreach ($argv as $iArg => $sArg) {
- if ($iArg == 0) {
- continue;
- } // Skip program name
- if (preg_match('/^--([A-Za-z0-9_]+)$/', $sArg, $aMatches)) {
- // Looks like a boolean parameter
- if (!array_key_exists($aMatches[1], $aOptionalParams) || ($aOptionalParams[$aMatches[1]] != 'boolean')) {
- $aUnknownParams[] = $sArg;
- }
- } else if (preg_match('/^--([A-Za-z0-9_]+)=(.*)$/', $sArg, $aMatches)) {
- // Looks like a regular parameter
- if (!array_key_exists($aMatches[1], $aOptionalParams) || ($aOptionalParams[$aMatches[1]] == 'boolean')) {
- $aUnknownParams[] = $sArg;
- }
- } else {
- $aUnknownParams[] = $sArg;
- }
- }
- }
-
- return $aUnknownParams;
- }
-
- /**
- * Init the console log level.
- *
- * Defaults to LOG_INFO if `console_log_level` is not configured
- * Can be overridden by `console_log_level` commandline argument.
- *
- * @throws Exception
- */
- public static function InitConsoleLogLevel()
- {
- $iDefaultConsoleLogLevel = static::GetConfigurationValue('console_log_level', LOG_INFO);
- static::$iConsoleLogLevel = static::ReadParameter('console_log_level', $iDefaultConsoleLogLevel);
- }
-
- /**
- * Logs a message to the centralized log for the application, with the given priority
- *
- * @param int $iPriority Use the LOG_* constants for priority e.g. LOG_WARNING, LOG_INFO, LOG_ERR... (see:
- * www.php.net/manual/en/function.syslog.php)
- * @param string $sMessage The message to log
- *
- * @return void
- * @throws \Exception
- */
- static public function Log($iPriority, $sMessage)
- {
- //testing only LOG_ERR
- if (self::$oMockedLogger) {
- if ($iPriority <= self::$iMockLogLevel) {
- var_dump($sMessage);
- self::$oMockedLogger->Log($iPriority, $sMessage);
- }
- }
-
- switch ($iPriority) {
- case LOG_EMERG:
- $sPrio = 'Emergency';
- break;
-
- case LOG_ALERT:
- $sPrio = 'Alert';
- break;
- case LOG_CRIT:
- $sPrio = 'Critical Error';
- break;
-
- case LOG_ERR:
- $sPrio = 'Error';
- break;
-
- case LOG_WARNING:
- $sPrio = 'Warning';
- break;
-
- case LOG_NOTICE:
- $sPrio = 'Notice';
- break;
-
- case LOG_INFO:
- $sPrio = 'Info';
- break;
-
- case LOG_DEBUG:
- $sPrio = 'Debug';
- break;
-
- default:
- $sPrio = 'Critical Error';
- }
-
- if ($iPriority <= self::$iConsoleLogLevel) {
- $log_date_format = self::GetConfigurationValue("console_log_dateformat", "[Y-m-d H:i:s]");
- $txt = date($log_date_format)."\t[".$sPrio."]\t".$sMessage."\n";
- echo $txt;
- }
-
- if ($iPriority <= self::$iSyslogLogLevel) {
- openlog('iTop Data Collector', LOG_PID, LOG_USER);
- syslog($iPriority, $sMessage);
- closelog();
- }
-
- if ($iPriority <= self::$iEventIssueLogLevel) {
- Utils::CreateEventIssue($sMessage);
- }
- }
-
- /**
- * @param bool $bResult
- * @param string $sErrorMessage
- */
- private static function CreateEventIssue($sMessage)
- {
- $sProjectName = self::$sProjectName;
- $sCollectorName = (self::$oCollector == null) ? "" : get_class(self::$oCollector);
- $sStep = self::$sStep;
-
- $aFields = [
- "message" => "$sMessage",
- "userinfo" => "Collector",
- "issue" => "$sStep-$sCollectorName",
- "impact" => "$sProjectName",
- ];
-
- $oClient = new RestClient();
- $oClient->Create("EventIssue", $aFields, "create event issue from collector $sCollectorName execution.");
- }
-
- static public function MockLog($oMockedLogger, $iMockLogLevel = LOG_ERR)
- {
- self::$oMockedLogger = $oMockedLogger;
- self::$iMockLogLevel = $iMockLogLevel;
- }
-
- /**
- * @param DoPostRequestService|null $oMockedDoPostRequestService
- * @since 1.3.0 N°6012
- * @return void
- */
- static public function MockDoPostRequestService($oMockedDoPostRequestService)
- {
- self::$oMockedDoPostRequestService = $oMockedDoPostRequestService;
- }
-
- /**
- * Load the configuration from the various XML configuration files
- *
- * @return Parameters
- * @throws Exception
- */
- static public function LoadConfig()
- {
- $sCustomConfigFile = Utils::ReadParameter('config_file', null);
-
- self::$aConfigFiles[] = CONF_DIR.'params.distrib.xml';
- self::$oConfig = new Parameters(CONF_DIR.'params.distrib.xml');
- if (file_exists(APPROOT.'collectors/params.distrib.xml')) {
- self::MergeConfFile(APPROOT.'collectors/params.distrib.xml');
- }
- if (file_exists(APPROOT.'collectors/extensions/params.distrib.xml')) {
- self::MergeConfFile(APPROOT.'collectors/extensions/params.distrib.xml');
- }
- if ($sCustomConfigFile !== null) {
- // A custom config file was supplied on the command line
- if (file_exists($sCustomConfigFile)) {
- self::MergeConfFile($sCustomConfigFile);
- } else {
- throw new Exception("The specified configuration file '$sCustomConfigFile' does not exist.");
- }
- } else if (file_exists(CONF_DIR.'params.local.xml')) {
- self::MergeConfFile(CONF_DIR.'params.local.xml');
- }
-
- return self::$oConfig;
- }
-
- static private function MergeConfFile($sFilePath)
- {
- self::$aConfigFiles[] = $sFilePath;
- $oLocalConfig = new Parameters($sFilePath);
- self::$oConfig->Merge($oLocalConfig);
- }
-
- /**
- * Get the value of a configuration parameter
- *
- * @param string $sCode
- * @param mixed $defaultValue
- *
- * @return mixed
- * @throws Exception
- */
- static public function GetConfigurationValue($sCode, $defaultValue = '')
- {
- if (self::$oConfig == null) {
- self::LoadConfig();
- }
-
- $value = self::$oConfig->Get($sCode, $defaultValue);
- $value = self::Substitute($value);
-
- return $value;
- }
-
- /**
- * @since 1.3.0 N°6012
- */
- static public function GetCredentials() : array {
- $sToken = Utils::GetConfigurationValue('itop_token', '');
- if (strlen($sToken) > 0){
- return [
- 'auth_token' => $sToken
- ];
- }
-
- return [
- 'auth_user' => Utils::GetConfigurationValue('itop_login', ''),
- 'auth_pwd' => Utils::GetConfigurationValue('itop_password', ''),
- ];
- }
-
- /**
- * @since 1.3.0 N°6012
- */
- static public function GetLoginMode() : string {
- $sLoginform = Utils::GetConfigurationValue('itop_login_mode', '');
- if (strlen($sLoginform) > 0){
- return $sLoginform;
- }
-
- $sToken = Utils::GetConfigurationValue('itop_token', '');
- if (strlen($sToken) > 0){
- return 'token';
- }
-
- return 'form';
- }
-
- /**
- * Dump information about the configuration (value of the parameters)
- *
- * @return string
- * @throws Exception
- */
- static public function DumpConfig()
- {
- if (self::$oConfig == null) {
- self::LoadConfig();
- }
-
- return self::$oConfig->Dump();
- }
-
- /**
- * Get the ordered list of configuration files loaded
- *
- * @return string
- * @throws Exception
- */
- static public function GetConfigFiles()
- {
- if (self::$oConfig == null) {
- self::LoadConfig();
- }
-
- return self::$aConfigFiles;
- }
-
- static protected function Substitute($value)
- {
- if (is_array($value)) {
- // Recursiverly process each entry
- foreach ($value as $key => $val) {
- $value[$key] = self::Substitute($val);
- }
- } else if (is_string($value)) {
- preg_match_all('/\$([A-Za-z0-9-_]+)\$/', $value, $aMatches);
- $aReplacements = array();
- if (count($aMatches) > 0) {
- foreach ($aMatches[1] as $sSubCode) {
- $aReplacements['$'.$sSubCode.'$'] = self::GetConfigurationValue($sSubCode, '#ERROR_UNDEFINED_PLACEHOLDER_'.$sSubCode.'#');
- }
- $value = str_replace(array_keys($aReplacements), $aReplacements, $value);
- }
- } else {
- // Do nothing, return as-is
- }
-
- return $value;
- }
-
- /**
- * Return the (valid) location where to store some temporary data
- * Throws an exception if the directory specified in the 'data_path' configuration does not exist and cannot be created
- *
- * @param string $sFileName
- *
- * @return string
- * @throws Exception
- */
- static public function GetDataFilePath($sFileName)
- {
- $sPath = static::GetConfigurationValue('data_path', '%APPROOT%/data/');
- $sPath = str_replace('%APPROOT%', APPROOT, $sPath); // substitute the %APPROOT% placeholder with its actual value
- $sPath = rtrim($sPath, '/').'/'; // Make that the path ends with exactly one /
- if (!file_exists($sPath)) {
- if (!mkdir($sPath, 0700, true)) {
- throw new Exception("Failed to create data_path: '$sPath'. Either create the directory yourself or make sure that the script has enough rights to create it.");
- }
- }
-
- return $sPath.basename($sFileName);
- }
-
- /**
- * Helper to execute an HTTP POST request
- * Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl
- * originaly named after do_post_request
- * Does not require cUrl but requires openssl for performing https POSTs.
- *
- * @param string $sUrl The URL to POST the data to
- * @param hash $aData The data to POST as an array('param_name' => value)
- * @param string $sOptionnalHeaders Additional HTTP headers as a string with newlines between headers
- * @param hash $aResponseHeaders An array to be filled with reponse headers: WARNING: the actual content of the array depends on the library used: cURL or fopen, test with both !! See: http://fr.php.net/manual/en/function.curl-getinfo.php
- * @param hash $aCurlOptions An (optional) array of options to pass to curl_init. The format is 'option_code' => 'value'. These values have precedence over the default ones
- *
- * @return string The result of the POST request
- * @throws Exception
- */
- static public function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = [])
- {
- if (self::$oMockedDoPostRequestService) {
- return self::$oMockedDoPostRequestService->DoPostRequest($sUrl, $aData, $sOptionnalHeaders, $aResponseHeaders, $aCurlOptions);
- }
-
- // $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request.
-
- if (function_exists('curl_init')) {
- // If cURL is available, let's use it, since it provides a greater control over the various HTTP/SSL options
- // For instance fopen does not allow to work around the bug: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112
- // by setting the SSLVERSION to 3 as done below.
- $aHeaders = explode("\n", $sOptionnalHeaders);
- // N°3267 - Webservices: Fix optional headers not being taken into account
- // See https://www.php.net/curl_setopt CURLOPT_HTTPHEADER
- $aHTTPHeaders = array();
- foreach ($aHeaders as $sHeaderString) {
- $aHTTPHeaders[] = trim($sHeaderString);
- }
- // Default options, can be overloaded/extended with the 4th parameter of this method, see above $aCurlOptions
- $aOptions = array(
- CURLOPT_RETURNTRANSFER => true, // return the content of the request
- CURLOPT_HEADER => false, // don't return the headers in the output
- CURLOPT_FOLLOWLOCATION => true, // follow redirects
- CURLOPT_ENCODING => "", // handle all encodings
- CURLOPT_USERAGENT => "spider", // who am i
- CURLOPT_AUTOREFERER => true, // set referer on redirect
- CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect
- CURLOPT_TIMEOUT => 120, // timeout on response
- CURLOPT_MAXREDIRS => 10, // stop after 10 redirects
- CURLOPT_SSL_VERIFYHOST => 0, // Disabled SSL Cert checks
- CURLOPT_SSL_VERIFYPEER => 0, // Disabled SSL Cert checks
- // SSLV3 (CURL_SSLVERSION_SSLv3 = 3) is now considered as obsolete/dangerous: http://disablessl3.com/#why
- // but it used to be a MUST to prevent a strange SSL error: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112
- // CURLOPT_SSLVERSION => 3,
- CURLOPT_POST => count($aData),
- CURLOPT_POSTFIELDS => http_build_query($aData),
- CURLOPT_HTTPHEADER => $aHTTPHeaders,
- );
-
- $aAllOptions = $aCurlOptions + $aOptions;
- $ch = curl_init($sUrl);
- curl_setopt_array($ch, $aAllOptions);
- $response = curl_exec($ch);
- $iErr = curl_errno($ch);
- $sErrMsg = curl_error($ch);
- $aHeaders = curl_getinfo($ch);
- if ($iErr !== 0) {
- throw new IOException("Problem opening URL: $sUrl"
- .PHP_EOL." error msg: $sErrMsg"
- .PHP_EOL." curl_init error code: $iErr (cf https://www.php.net/manual/en/function.curl-errno.php)");
- }
- if (is_array($aResponseHeaders)) {
- $aHeaders = curl_getinfo($ch);
- foreach ($aHeaders as $sCode => $sValue) {
- $sName = str_replace(' ', '-', ucwords(str_replace('_', ' ', $sCode))); // Transform "content_type" into "Content-Type"
- $aResponseHeaders[$sName] = $sValue;
- }
- }
- curl_close($ch);
- } else {
- // cURL is not available let's try with streams and fopen...
-
- $sData = http_build_query($aData);
- $aParams = array(
- 'http' => array(
- 'method' => 'POST',
- 'content' => $sData,
- 'header' => "Content-type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($sData)."\r\n",
- ),
- );
- if ($sOptionnalHeaders !== null) {
- $aParams['http']['header'] .= $sOptionnalHeaders;
- }
- $ctx = stream_context_create($aParams);
-
- $fp = @fopen($sUrl, 'rb', false, $ctx);
- if (!$fp) {
- $error_arr = error_get_last();
- if (is_array($error_arr)) {
- throw new IOException("Wrong URL: $sUrl, Error: ".json_encode($error_arr));
- } elseif ((strtolower(substr($sUrl, 0, 5)) == 'https') && !extension_loaded('openssl')) {
- throw new IOException("Cannot connect to $sUrl: missing module 'openssl'");
- } else {
- throw new IOException("Wrong URL: $sUrl");
- }
- }
- $response = @stream_get_contents($fp);
- if ($response === false) {
- throw new IOException("Problem reading data from $sUrl, " . error_get_last() );
- }
- if (is_array($aResponseHeaders)) {
- $aMeta = stream_get_meta_data($fp);
- $aHeaders = $aMeta['wrapper_data'];
- foreach ($aHeaders as $sHeaderString) {
- if (preg_match('/^([^:]+): (.+)$/', $sHeaderString, $aMatches)) {
- $aResponseHeaders[$aMatches[1]] = trim($aMatches[2]);
- }
- }
- }
- }
-
- return $response;
- }
-
- /**
- * Pretty print a JSON formatted string. Copied/pasted from http://stackoverflow.com/questions/6054033/pretty-printing-json-with-php
- *
- * @deprecated 1.3.0 use `json_encode($value, JSON_PRETTY_PRINT);` instead (PHP 5.4.0 required)
- *
- * @param string $json A JSON formatted object definition
- *
- * @return string The nicely formatted JSON definition
- */
- public static function JSONPrettyPrint($json)
- {
- Utils::Log(LOG_NOTICE, 'Use of deprecated method '.__METHOD__);
-
- $result = '';
- $level = 0;
- $in_quotes = false;
- $in_escape = false;
- $ends_line_level = null;
- $json_length = strlen($json);
-
- for ($i = 0; $i < $json_length; $i++) {
- $char = $json[$i];
- $new_line_level = null;
- $post = "";
- if ($ends_line_level !== null) {
- $new_line_level = $ends_line_level;
- $ends_line_level = null;
- }
- if ($in_escape) {
- $in_escape = false;
- } else if ($char === '"') {
- $in_quotes = !$in_quotes;
- } else if (!$in_quotes) {
- switch ($char) {
- case '}':
- case ']':
- $level--;
- $ends_line_level = null;
- $new_line_level = $level;
- break;
-
- case '{':
- case '[':
- $level++;
- case ',':
- $ends_line_level = $level;
- break;
-
- case ':':
- $post = " ";
- break;
-
- case " ":
- case "\t":
- case "\n":
- case "\r":
- $char = "";
- $ends_line_level = $new_line_level;
- $new_line_level = null;
- break;
- }
- } else if ($char === '\\') {
- $in_escape = true;
- }
- if ($new_line_level !== null) {
- $result .= "\n".str_repeat("\t", $new_line_level);
- }
- $result .= $char.$post;
- }
-
- return $result;
- }
-
- /**
- * Executes a command and returns an array with exit code, stdout and stderr content
- *
- * @param string $cmd - Command to execute
- *
- * @return false|string
- * @throws \Exception
- */
- public static function Exec($sCmd)
- {
- $iBeginTime = time();
- $sWorkDir = APPROOT;
- $aDescriptorSpec = array(
- 0 => array("pipe", "r"), // stdin
- 1 => array("pipe", "w"), // stdout
- 2 => array("pipe", "w"), // stderr
- );
- Utils::Log(LOG_INFO, "Command: $sCmd. Workdir: $sWorkDir");
- $rProcess = proc_open($sCmd, $aDescriptorSpec, $aPipes, $sWorkDir, null);
-
- $sStdOut = stream_get_contents($aPipes[1]);
- fclose($aPipes[1]);
-
- $sStdErr = stream_get_contents($aPipes[2]);
- fclose($aPipes[2]);
-
- $iCode = proc_close($rProcess);
-
- $iElapsed = time() - $iBeginTime;
- if (0 === $iCode) {
- Utils::Log(LOG_INFO, "elapsed:{$iElapsed}s output: $sStdOut");
-
- return $sStdOut;
- } else {
- throw new Exception("Command failed : $sCmd \n\t\t=== with status:$iCode \n\t\t=== stderr:$sStdErr \n\t\t=== stdout: $sStdOut");
- }
- }
-
- /**
- * @since 1.3.0
- */
- public static function GetCurlOptions(int $iCurrentTimeOut=-1) : array
- {
- $aRawCurlOptions = Utils::GetConfigurationValue('curl_options', [CURLOPT_SSLVERSION => CURL_SSLVERSION_SSLv3]);
- return self::ComputeCurlOptions($aRawCurlOptions, $iCurrentTimeOut);
- }
-
- /**
- * @since 1.3.0
- */
- public static function ComputeCurlOptions(array $aRawCurlOptions, int $iCurrentTimeOut) : array
- {
- $aCurlOptions = array();
- foreach ($aRawCurlOptions as $key => $value) {
- // Convert strings like 'CURLOPT_SSLVERSION' to the value of the corresponding define i.e CURLOPT_SSLVERSION = 32 !
- $iKey = (!is_numeric($key)) ? constant((string)$key) : (int)$key;
- $aCurlOptions[$iKey] = (!is_numeric($value) && defined($value)) ? constant($value) : $value;
- }
-
- if ($iCurrentTimeOut !== -1) {
- $aCurlOptions[CURLOPT_CONNECTTIMEOUT] = $iCurrentTimeOut;
- $aCurlOptions[CURLOPT_TIMEOUT] = $iCurrentTimeOut;
- }
-
- return $aCurlOptions;
- }
-
- /**
- * Check if the given module is installed in iTop.
- * Mind that this assumes the `ModuleInstallation` class is ordered by descending installation date
- *
- * @param string $sModuleId Name of the module to be found, optionally included version (e.g. "some-module" or "some-module/1.2.3")
- * @param bool $bRequired Whether to throw exceptions when module not found
- * @param RestClient|null $oClient
- * @return bool True when the given module is installed, false otherwise
- * @throws Exception When the module is required but could not be found
- */
- public static function CheckModuleInstallation(string $sModuleId, bool $bRequired = false, RestClient $oClient = null): bool
- {
- if (!isset($oClient))
- {
- $oClient = new RestClient();
- }
-
- $sName = '';
- $sOperator = '';
- if (preg_match('/^([^\/]+)(?:\/([<>]?=?)(.+))?$/', $sModuleId, $aModuleMatches)) {
- $sName = $aModuleMatches[1];
- $sOperator = $aModuleMatches[2] ?? null ?: '>=';
- $sExpectedVersion = $aModuleMatches[3] ?? null;
- }
-
- try {
- if (!isset(static::$sLastInstallDate)) {
- $aDatamodelResults = $oClient->Get('ModuleInstallation', ['name' => 'datamodel'], 'installed', 1);
- if ($aDatamodelResults['code'] != 0 || empty($aDatamodelResults['objects'])){
- throw new Exception($aDatamodelResults['message'], $aDatamodelResults['code']);
- }
- $aDatamodel = current($aDatamodelResults['objects']);
- static::$sLastInstallDate = $aDatamodel['fields']['installed'];
- }
-
- $aResults = $oClient->Get('ModuleInstallation', ['name' => $sName, 'installed' => static::$sLastInstallDate], 'name,version', 1);
- if ($aResults['code'] != 0 || empty($aResults['objects'])) {
- throw new Exception($aResults['message'], $aResults['code']);
- }
- $aObject = current($aResults['objects']);
- $sCurrentVersion = $aObject['fields']['version'];
-
- if (isset($sExpectedVersion) && !version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) {
- throw new Exception(sprintf('Version mismatch (%s %s %s)', $sCurrentVersion, $sOperator, $sExpectedVersion));
- }
-
- Utils::Log(LOG_DEBUG, sprintf('iTop module %s version %s is installed.', $aObject['fields']['name'], $sCurrentVersion));
- } catch (Exception $e) {
- $sMessage = sprintf('%s iTop module %s is considered as not installed due to: %s', $bRequired ? 'Required' : 'Optional', $sName, $e->getMessage());
- if ($bRequired) {
- throw new Exception($sMessage, 0, $e);
- } else {
- Utils::Log(LOG_INFO, $sMessage);
- return false;
- }
- }
- return true;
- }
+ public static $iConsoleLogLevel = LOG_INFO;
+ public static $iSyslogLogLevel = LOG_NONE;
+ public static $iEventIssueLogLevel = LOG_NONE;
+ public static $sProjectName = "";
+ public static $sStep = "";
+ public static $oCollector = "";
+ protected static $oConfig = null;
+ protected static $aConfigFiles = [];
+ protected static $iMockLogLevel = LOG_ERR;
+
+ protected static $oMockedLogger;
+
+ /**
+ * @since 1.3.0 N°6012
+ */
+ protected static $oMockedDoPostRequestService;
+
+ /**
+ * @var string Keeps track of the latest date the datamodel has been installed/updated
+ * (in order to check which modules were installed with it)
+ */
+ protected static $sLastInstallDate;
+
+ public static function SetProjectName($sProjectName)
+ {
+ if ($sProjectName != null) {
+ self::$sProjectName = $sProjectName;
+ }
+ }
+
+ public static function SetCollector($oCollector, $sStep = "")
+ {
+ self::$oCollector = $oCollector;
+ self::$sStep = $sStep;
+ }
+
+ public static function ReadParameter($sParamName, $defaultValue)
+ {
+ global $argv;
+
+ $retValue = $defaultValue;
+ if (is_array($argv)) {
+ foreach ($argv as $iArg => $sArg) {
+ if (preg_match('/^--'.$sParamName.'=(.*)$/', $sArg, $aMatches)) {
+ $retValue = $aMatches[1];
+ }
+ }
+ }
+
+ return $retValue;
+ }
+
+ public static function ReadBooleanParameter($sParamName, $defaultValue)
+ {
+ global $argv;
+
+ $retValue = $defaultValue;
+ if (is_array($argv)) {
+ foreach ($argv as $iArg => $sArg) {
+ if (preg_match('/^--'.$sParamName.'$/', $sArg, $aMatches)) {
+ $retValue = true;
+ } elseif (preg_match('/^--'.$sParamName.'=(.*)$/', $sArg, $aMatches)) {
+ $retValue = ($aMatches[1] != 0);
+ }
+ }
+ }
+
+ return $retValue;
+ }
+
+ public static function CheckParameters($aOptionalParams)
+ {
+ global $argv;
+
+ $aUnknownParams = [];
+ if (is_array($argv)) {
+ foreach ($argv as $iArg => $sArg) {
+ if ($iArg == 0) {
+ continue;
+ } // Skip program name
+ if (preg_match('/^--([A-Za-z0-9_]+)$/', $sArg, $aMatches)) {
+ // Looks like a boolean parameter
+ if (!array_key_exists($aMatches[1], $aOptionalParams) || ($aOptionalParams[$aMatches[1]] != 'boolean')) {
+ $aUnknownParams[] = $sArg;
+ }
+ } elseif (preg_match('/^--([A-Za-z0-9_]+)=(.*)$/', $sArg, $aMatches)) {
+ // Looks like a regular parameter
+ if (!array_key_exists($aMatches[1], $aOptionalParams) || ($aOptionalParams[$aMatches[1]] == 'boolean')) {
+ $aUnknownParams[] = $sArg;
+ }
+ } else {
+ $aUnknownParams[] = $sArg;
+ }
+ }
+ }
+
+ return $aUnknownParams;
+ }
+
+ /**
+ * Init the console log level.
+ *
+ * Defaults to LOG_INFO if `console_log_level` is not configured
+ * Can be overridden by `console_log_level` commandline argument.
+ *
+ * @throws Exception
+ */
+ public static function InitConsoleLogLevel()
+ {
+ $iDefaultConsoleLogLevel = static::GetConfigurationValue('console_log_level', LOG_INFO);
+ static::$iConsoleLogLevel = static::ReadParameter('console_log_level', $iDefaultConsoleLogLevel);
+ }
+
+ /**
+ * Logs a message to the centralized log for the application, with the given priority
+ *
+ * @param int $iPriority Use the LOG_* constants for priority e.g. LOG_WARNING, LOG_INFO, LOG_ERR... (see:
+ * www.php.net/manual/en/function.syslog.php)
+ * @param string $sMessage The message to log
+ *
+ * @return void
+ * @throws \Exception
+ */
+ public static function Log($iPriority, $sMessage)
+ {
+ //testing only LOG_ERR
+ if (self::$oMockedLogger) {
+ if ($iPriority <= self::$iMockLogLevel) {
+ var_dump($sMessage);
+ self::$oMockedLogger->Log($iPriority, $sMessage);
+ }
+ }
+
+ switch ($iPriority) {
+ case LOG_EMERG:
+ $sPrio = 'Emergency';
+ break;
+
+ case LOG_ALERT:
+ $sPrio = 'Alert';
+ break;
+ case LOG_CRIT:
+ $sPrio = 'Critical Error';
+ break;
+
+ case LOG_ERR:
+ $sPrio = 'Error';
+ break;
+
+ case LOG_WARNING:
+ $sPrio = 'Warning';
+ break;
+
+ case LOG_NOTICE:
+ $sPrio = 'Notice';
+ break;
+
+ case LOG_INFO:
+ $sPrio = 'Info';
+ break;
+
+ case LOG_DEBUG:
+ $sPrio = 'Debug';
+ break;
+
+ default:
+ $sPrio = 'Critical Error';
+ }
+
+ if ($iPriority <= self::$iConsoleLogLevel) {
+ $log_date_format = self::GetConfigurationValue("console_log_dateformat", "[Y-m-d H:i:s]");
+ $txt = date($log_date_format)."\t[".$sPrio."]\t".$sMessage."\n";
+ echo $txt;
+ }
+
+ if ($iPriority <= self::$iSyslogLogLevel) {
+ openlog('iTop Data Collector', LOG_PID, LOG_USER);
+ syslog($iPriority, $sMessage);
+ closelog();
+ }
+
+ if ($iPriority <= self::$iEventIssueLogLevel) {
+ Utils::CreateEventIssue($sMessage);
+ }
+ }
+
+ /**
+ * @param bool $bResult
+ * @param string $sErrorMessage
+ */
+ private static function CreateEventIssue($sMessage)
+ {
+ $sProjectName = self::$sProjectName;
+ $sCollectorName = (self::$oCollector == null) ? "" : get_class(self::$oCollector);
+ $sStep = self::$sStep;
+
+ $aFields = [
+ "message" => "$sMessage",
+ "userinfo" => "Collector",
+ "issue" => "$sStep-$sCollectorName",
+ "impact" => "$sProjectName",
+ ];
+
+ $oClient = new RestClient();
+ $oClient->Create("EventIssue", $aFields, "create event issue from collector $sCollectorName execution.");
+ }
+
+ public static function MockLog($oMockedLogger, $iMockLogLevel = LOG_ERR)
+ {
+ self::$oMockedLogger = $oMockedLogger;
+ self::$iMockLogLevel = $iMockLogLevel;
+ }
+
+ /**
+ * @param DoPostRequestService|null $oMockedDoPostRequestService
+ * @since 1.3.0 N°6012
+ * @return void
+ */
+ public static function MockDoPostRequestService($oMockedDoPostRequestService)
+ {
+ self::$oMockedDoPostRequestService = $oMockedDoPostRequestService;
+ }
+
+ /**
+ * Load the configuration from the various XML configuration files
+ *
+ * @return Parameters
+ * @throws Exception
+ */
+ public static function LoadConfig()
+ {
+ $sCustomConfigFile = Utils::ReadParameter('config_file', null);
+
+ self::$aConfigFiles[] = CONF_DIR.'params.distrib.xml';
+ self::$oConfig = new Parameters(CONF_DIR.'params.distrib.xml');
+ if (file_exists(APPROOT.'collectors/params.distrib.xml')) {
+ self::MergeConfFile(APPROOT.'collectors/params.distrib.xml');
+ }
+ if (file_exists(APPROOT.'collectors/extensions/params.distrib.xml')) {
+ self::MergeConfFile(APPROOT.'collectors/extensions/params.distrib.xml');
+ }
+ if ($sCustomConfigFile !== null) {
+ // A custom config file was supplied on the command line
+ if (file_exists($sCustomConfigFile)) {
+ self::MergeConfFile($sCustomConfigFile);
+ } else {
+ throw new Exception("The specified configuration file '$sCustomConfigFile' does not exist.");
+ }
+ } elseif (file_exists(CONF_DIR.'params.local.xml')) {
+ self::MergeConfFile(CONF_DIR.'params.local.xml');
+ }
+
+ return self::$oConfig;
+ }
+
+ private static function MergeConfFile($sFilePath)
+ {
+ self::$aConfigFiles[] = $sFilePath;
+ $oLocalConfig = new Parameters($sFilePath);
+ self::$oConfig->Merge($oLocalConfig);
+ }
+
+ /**
+ * Get the value of a configuration parameter
+ *
+ * @param string $sCode
+ * @param mixed $defaultValue
+ *
+ * @return mixed
+ * @throws Exception
+ */
+ public static function GetConfigurationValue($sCode, $defaultValue = '')
+ {
+ if (self::$oConfig == null) {
+ self::LoadConfig();
+ }
+
+ $value = self::$oConfig->Get($sCode, $defaultValue);
+ $value = self::Substitute($value);
+
+ return $value;
+ }
+
+ /**
+ * @since 1.3.0 N°6012
+ */
+ public static function GetCredentials(): array
+ {
+ $sToken = Utils::GetConfigurationValue('itop_token', '');
+ if (strlen($sToken) > 0) {
+ return [
+ 'auth_token' => $sToken
+ ];
+ }
+
+ return [
+ 'auth_user' => Utils::GetConfigurationValue('itop_login', ''),
+ 'auth_pwd' => Utils::GetConfigurationValue('itop_password', ''),
+ ];
+ }
+
+ /**
+ * @since 1.3.0 N°6012
+ */
+ public static function GetLoginMode(): string
+ {
+ $sLoginform = Utils::GetConfigurationValue('itop_login_mode', '');
+ if (strlen($sLoginform) > 0) {
+ return $sLoginform;
+ }
+
+ $sToken = Utils::GetConfigurationValue('itop_token', '');
+ if (strlen($sToken) > 0) {
+ return 'token';
+ }
+
+ return 'form';
+ }
+
+ /**
+ * Dump information about the configuration (value of the parameters)
+ *
+ * @return string
+ * @throws Exception
+ */
+ public static function DumpConfig()
+ {
+ if (self::$oConfig == null) {
+ self::LoadConfig();
+ }
+
+ return self::$oConfig->Dump();
+ }
+
+ /**
+ * Get the ordered list of configuration files loaded
+ *
+ * @return string
+ * @throws Exception
+ */
+ public static function GetConfigFiles()
+ {
+ if (self::$oConfig == null) {
+ self::LoadConfig();
+ }
+
+ return self::$aConfigFiles;
+ }
+
+ protected static function Substitute($value)
+ {
+ if (is_array($value)) {
+ // Recursiverly process each entry
+ foreach ($value as $key => $val) {
+ $value[$key] = self::Substitute($val);
+ }
+ } elseif (is_string($value)) {
+ preg_match_all('/\$([A-Za-z0-9-_]+)\$/', $value, $aMatches);
+ $aReplacements = [];
+ if (count($aMatches) > 0) {
+ foreach ($aMatches[1] as $sSubCode) {
+ $aReplacements['$'.$sSubCode.'$'] = self::GetConfigurationValue($sSubCode, '#ERROR_UNDEFINED_PLACEHOLDER_'.$sSubCode.'#');
+ }
+ $value = str_replace(array_keys($aReplacements), $aReplacements, $value);
+ }
+ } else {
+ // Do nothing, return as-is
+ }
+
+ return $value;
+ }
+
+ /**
+ * Return the (valid) location where to store some temporary data
+ * Throws an exception if the directory specified in the 'data_path' configuration does not exist and cannot be created
+ *
+ * @param string $sFileName
+ *
+ * @return string
+ * @throws Exception
+ */
+ public static function GetDataFilePath($sFileName)
+ {
+ $sPath = static::GetConfigurationValue('data_path', '%APPROOT%/data/');
+ $sPath = str_replace('%APPROOT%', APPROOT, $sPath); // substitute the %APPROOT% placeholder with its actual value
+ $sPath = rtrim($sPath, '/').'/'; // Make that the path ends with exactly one /
+ if (!file_exists($sPath)) {
+ if (!mkdir($sPath, 0700, true)) {
+ throw new Exception("Failed to create data_path: '$sPath'. Either create the directory yourself or make sure that the script has enough rights to create it.");
+ }
+ }
+
+ return $sPath.basename($sFileName);
+ }
+
+ /**
+ * Helper to execute an HTTP POST request
+ * Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl
+ * originaly named after do_post_request
+ * Does not require cUrl but requires openssl for performing https POSTs.
+ *
+ * @param string $sUrl The URL to POST the data to
+ * @param hash $aData The data to POST as an array('param_name' => value)
+ * @param string $sOptionnalHeaders Additional HTTP headers as a string with newlines between headers
+ * @param hash $aResponseHeaders An array to be filled with reponse headers: WARNING: the actual content of the array depends on the library used: cURL or fopen, test with both !! See: http://fr.php.net/manual/en/function.curl-getinfo.php
+ * @param hash $aCurlOptions An (optional) array of options to pass to curl_init. The format is 'option_code' => 'value'. These values have precedence over the default ones
+ *
+ * @return string The result of the POST request
+ * @throws Exception
+ */
+ public static function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = [])
+ {
+ if (self::$oMockedDoPostRequestService) {
+ return self::$oMockedDoPostRequestService->DoPostRequest($sUrl, $aData, $sOptionnalHeaders, $aResponseHeaders, $aCurlOptions);
+ }
+
+ // $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request.
+
+ if (function_exists('curl_init')) {
+ // If cURL is available, let's use it, since it provides a greater control over the various HTTP/SSL options
+ // For instance fopen does not allow to work around the bug: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112
+ // by setting the SSLVERSION to 3 as done below.
+ $aHeaders = explode("\n", $sOptionnalHeaders);
+ // N°3267 - Webservices: Fix optional headers not being taken into account
+ // See https://www.php.net/curl_setopt CURLOPT_HTTPHEADER
+ $aHTTPHeaders = [];
+ foreach ($aHeaders as $sHeaderString) {
+ $aHTTPHeaders[] = trim($sHeaderString);
+ }
+ // Default options, can be overloaded/extended with the 4th parameter of this method, see above $aCurlOptions
+ $aOptions = [
+ CURLOPT_RETURNTRANSFER => true, // return the content of the request
+ CURLOPT_HEADER => false, // don't return the headers in the output
+ CURLOPT_FOLLOWLOCATION => true, // follow redirects
+ CURLOPT_ENCODING => "", // handle all encodings
+ CURLOPT_USERAGENT => "spider", // who am i
+ CURLOPT_AUTOREFERER => true, // set referer on redirect
+ CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect
+ CURLOPT_TIMEOUT => 120, // timeout on response
+ CURLOPT_MAXREDIRS => 10, // stop after 10 redirects
+ CURLOPT_SSL_VERIFYHOST => 0, // Disabled SSL Cert checks
+ CURLOPT_SSL_VERIFYPEER => 0, // Disabled SSL Cert checks
+ // SSLV3 (CURL_SSLVERSION_SSLv3 = 3) is now considered as obsolete/dangerous: http://disablessl3.com/#why
+ // but it used to be a MUST to prevent a strange SSL error: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112
+ // CURLOPT_SSLVERSION => 3,
+ CURLOPT_POST => count($aData),
+ CURLOPT_POSTFIELDS => http_build_query($aData),
+ CURLOPT_HTTPHEADER => $aHTTPHeaders,
+ ];
+
+ $aAllOptions = $aCurlOptions + $aOptions;
+ $ch = curl_init($sUrl);
+ curl_setopt_array($ch, $aAllOptions);
+ $response = curl_exec($ch);
+ $iErr = curl_errno($ch);
+ $sErrMsg = curl_error($ch);
+ $aHeaders = curl_getinfo($ch);
+ if ($iErr !== 0) {
+ throw new IOException("Problem opening URL: $sUrl"
+ .PHP_EOL." error msg: $sErrMsg"
+ .PHP_EOL." curl_init error code: $iErr (cf https://www.php.net/manual/en/function.curl-errno.php)");
+ }
+ if (is_array($aResponseHeaders)) {
+ $aHeaders = curl_getinfo($ch);
+ foreach ($aHeaders as $sCode => $sValue) {
+ $sName = str_replace(' ', '-', ucwords(str_replace('_', ' ', $sCode))); // Transform "content_type" into "Content-Type"
+ $aResponseHeaders[$sName] = $sValue;
+ }
+ }
+ curl_close($ch);
+ } else {
+ // cURL is not available let's try with streams and fopen...
+
+ $sData = http_build_query($aData);
+ $aParams = [
+ 'http' => [
+ 'method' => 'POST',
+ 'content' => $sData,
+ 'header' => "Content-type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($sData)."\r\n",
+ ],
+ ];
+ if ($sOptionnalHeaders !== null) {
+ $aParams['http']['header'] .= $sOptionnalHeaders;
+ }
+ $ctx = stream_context_create($aParams);
+
+ $fp = @fopen($sUrl, 'rb', false, $ctx);
+ if (!$fp) {
+ $error_arr = error_get_last();
+ if (is_array($error_arr)) {
+ throw new IOException("Wrong URL: $sUrl, Error: ".json_encode($error_arr));
+ } elseif ((strtolower(substr($sUrl, 0, 5)) == 'https') && !extension_loaded('openssl')) {
+ throw new IOException("Cannot connect to $sUrl: missing module 'openssl'");
+ } else {
+ throw new IOException("Wrong URL: $sUrl");
+ }
+ }
+ $response = @stream_get_contents($fp);
+ if ($response === false) {
+ throw new IOException("Problem reading data from $sUrl, " . error_get_last());
+ }
+ if (is_array($aResponseHeaders)) {
+ $aMeta = stream_get_meta_data($fp);
+ $aHeaders = $aMeta['wrapper_data'];
+ foreach ($aHeaders as $sHeaderString) {
+ if (preg_match('/^([^:]+): (.+)$/', $sHeaderString, $aMatches)) {
+ $aResponseHeaders[$aMatches[1]] = trim($aMatches[2]);
+ }
+ }
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Pretty print a JSON formatted string. Copied/pasted from http://stackoverflow.com/questions/6054033/pretty-printing-json-with-php
+ *
+ * @deprecated 1.3.0 use `json_encode($value, JSON_PRETTY_PRINT);` instead (PHP 5.4.0 required)
+ *
+ * @param string $json A JSON formatted object definition
+ *
+ * @return string The nicely formatted JSON definition
+ */
+ public static function JSONPrettyPrint($json)
+ {
+ Utils::Log(LOG_NOTICE, 'Use of deprecated method '.__METHOD__);
+
+ $result = '';
+ $level = 0;
+ $in_quotes = false;
+ $in_escape = false;
+ $ends_line_level = null;
+ $json_length = strlen($json);
+
+ for ($i = 0; $i < $json_length; $i++) {
+ $char = $json[$i];
+ $new_line_level = null;
+ $post = "";
+ if ($ends_line_level !== null) {
+ $new_line_level = $ends_line_level;
+ $ends_line_level = null;
+ }
+ if ($in_escape) {
+ $in_escape = false;
+ } elseif ($char === '"') {
+ $in_quotes = !$in_quotes;
+ } elseif (!$in_quotes) {
+ switch ($char) {
+ case '}':
+ case ']':
+ $level--;
+ $ends_line_level = null;
+ $new_line_level = $level;
+ break;
+
+ case '{':
+ case '[':
+ $level++;
+ // no break
+ case ',':
+ $ends_line_level = $level;
+ break;
+
+ case ':':
+ $post = " ";
+ break;
+
+ case " ":
+ case "\t":
+ case "\n":
+ case "\r":
+ $char = "";
+ $ends_line_level = $new_line_level;
+ $new_line_level = null;
+ break;
+ }
+ } elseif ($char === '\\') {
+ $in_escape = true;
+ }
+ if ($new_line_level !== null) {
+ $result .= "\n".str_repeat("\t", $new_line_level);
+ }
+ $result .= $char.$post;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Executes a command and returns an array with exit code, stdout and stderr content
+ *
+ * @param string $cmd - Command to execute
+ *
+ * @return false|string
+ * @throws \Exception
+ */
+ public static function Exec($sCmd)
+ {
+ $iBeginTime = time();
+ $sWorkDir = APPROOT;
+ $aDescriptorSpec = [
+ 0 => ["pipe", "r"], // stdin
+ 1 => ["pipe", "w"], // stdout
+ 2 => ["pipe", "w"], // stderr
+ ];
+ Utils::Log(LOG_INFO, "Command: $sCmd. Workdir: $sWorkDir");
+ $rProcess = proc_open($sCmd, $aDescriptorSpec, $aPipes, $sWorkDir, null);
+
+ $sStdOut = stream_get_contents($aPipes[1]);
+ fclose($aPipes[1]);
+
+ $sStdErr = stream_get_contents($aPipes[2]);
+ fclose($aPipes[2]);
+
+ $iCode = proc_close($rProcess);
+
+ $iElapsed = time() - $iBeginTime;
+ if (0 === $iCode) {
+ Utils::Log(LOG_INFO, "elapsed:{$iElapsed}s output: $sStdOut");
+
+ return $sStdOut;
+ } else {
+ throw new Exception("Command failed : $sCmd \n\t\t=== with status:$iCode \n\t\t=== stderr:$sStdErr \n\t\t=== stdout: $sStdOut");
+ }
+ }
+
+ /**
+ * @since 1.3.0
+ */
+ public static function GetCurlOptions(int $iCurrentTimeOut = -1): array
+ {
+ $aRawCurlOptions = Utils::GetConfigurationValue('curl_options', [CURLOPT_SSLVERSION => CURL_SSLVERSION_SSLv3]);
+ return self::ComputeCurlOptions($aRawCurlOptions, $iCurrentTimeOut);
+ }
+
+ /**
+ * @since 1.3.0
+ */
+ public static function ComputeCurlOptions(array $aRawCurlOptions, int $iCurrentTimeOut): array
+ {
+ $aCurlOptions = [];
+ foreach ($aRawCurlOptions as $key => $value) {
+ // Convert strings like 'CURLOPT_SSLVERSION' to the value of the corresponding define i.e CURLOPT_SSLVERSION = 32 !
+ $iKey = (!is_numeric($key)) ? constant((string)$key) : (int)$key;
+ $aCurlOptions[$iKey] = (!is_numeric($value) && defined($value)) ? constant($value) : $value;
+ }
+
+ if ($iCurrentTimeOut !== -1) {
+ $aCurlOptions[CURLOPT_CONNECTTIMEOUT] = $iCurrentTimeOut;
+ $aCurlOptions[CURLOPT_TIMEOUT] = $iCurrentTimeOut;
+ }
+
+ return $aCurlOptions;
+ }
+
+ /**
+ * Check if the given module is installed in iTop.
+ * Mind that this assumes the `ModuleInstallation` class is ordered by descending installation date
+ *
+ * @param string $sModuleId Name of the module to be found, optionally included version (e.g. "some-module" or "some-module/1.2.3")
+ * @param bool $bRequired Whether to throw exceptions when module not found
+ * @param RestClient|null $oClient
+ * @return bool True when the given module is installed, false otherwise
+ * @throws Exception When the module is required but could not be found
+ */
+ public static function CheckModuleInstallation(string $sModuleId, bool $bRequired = false, RestClient $oClient = null): bool
+ {
+ if (!isset($oClient)) {
+ $oClient = new RestClient();
+ }
+
+ $sName = '';
+ $sOperator = '';
+ if (preg_match('/^([^\/]+)(?:\/([<>]?=?)(.+))?$/', $sModuleId, $aModuleMatches)) {
+ $sName = $aModuleMatches[1];
+ $sOperator = $aModuleMatches[2] ?? null ?: '>=';
+ $sExpectedVersion = $aModuleMatches[3] ?? null;
+ }
+
+ try {
+ if (!isset(static::$sLastInstallDate)) {
+ $aDatamodelResults = $oClient->Get('ModuleInstallation', ['name' => 'datamodel'], 'installed', 1);
+ if ($aDatamodelResults['code'] != 0 || empty($aDatamodelResults['objects'])) {
+ throw new Exception($aDatamodelResults['message'], $aDatamodelResults['code']);
+ }
+ $aDatamodel = current($aDatamodelResults['objects']);
+ static::$sLastInstallDate = $aDatamodel['fields']['installed'];
+ }
+
+ $aResults = $oClient->Get('ModuleInstallation', ['name' => $sName, 'installed' => static::$sLastInstallDate], 'name,version', 1);
+ if ($aResults['code'] != 0 || empty($aResults['objects'])) {
+ throw new Exception($aResults['message'], $aResults['code']);
+ }
+ $aObject = current($aResults['objects']);
+ $sCurrentVersion = $aObject['fields']['version'];
+
+ if (isset($sExpectedVersion) && !version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) {
+ throw new Exception(sprintf('Version mismatch (%s %s %s)', $sCurrentVersion, $sOperator, $sExpectedVersion));
+ }
+
+ Utils::Log(LOG_DEBUG, sprintf('iTop module %s version %s is installed.', $aObject['fields']['name'], $sCurrentVersion));
+ } catch (Exception $e) {
+ $sMessage = sprintf('%s iTop module %s is considered as not installed due to: %s', $bRequired ? 'Required' : 'Optional', $sName, $e->getMessage());
+ if ($bRequired) {
+ throw new Exception($sMessage, 0, $e);
+ } else {
+ Utils::Log(LOG_INFO, $sMessage);
+ return false;
+ }
+ }
+ return true;
+ }
}
class UtilsLogger
{
- /**
- * UtilsLogger constructor.
- */
- public function __construct()
- {
- }
-
- public function Log($iPriority, $sMessage)
- {
- }
+ /**
+ * UtilsLogger constructor.
+ */
+ public function __construct()
+ {
+ }
+
+ public function Log($iPriority, $sMessage)
+ {
+ }
}
diff --git a/exec.php b/exec.php
index a98218a..645eeeb 100644
--- a/exec.php
+++ b/exec.php
@@ -1,4 +1,5 @@
'boolean',
- 'collect_only' => 'boolean',
- 'synchro_only' => 'boolean',
- 'dump_config_only' => 'boolean',
- 'console_log_level' => 'integer',
- 'eventissue_log_level' => 'integer',
- 'max_chunk_size' => 'integer',
- 'help' => 'boolean',
- 'config_file' => 'string',
-);
+$aOptionalParams = [
+ 'configure_only' => 'boolean',
+ 'collect_only' => 'boolean',
+ 'synchro_only' => 'boolean',
+ 'dump_config_only' => 'boolean',
+ 'console_log_level' => 'integer',
+ 'eventissue_log_level' => 'integer',
+ 'max_chunk_size' => 'integer',
+ 'help' => 'boolean',
+ 'config_file' => 'string',
+];
$bHelp = (Utils::ReadBooleanParameter('help', false) == true);
$aUnknownParameters = Utils::CheckParameters($aOptionalParams);
if ($bHelp || count($aUnknownParameters) > 0) {
- if (!$bHelp) {
- Utils::Log(LOG_ERR, "Unknown parameter(s): ".implode(' ', $aUnknownParameters));
- }
-
- echo "Usage:\n";
- echo 'php '.basename($argv[0]);
- foreach ($aOptionalParams as $sParam => $sType) {
- switch ($sType) {
- case 'boolean':
- echo '[--'.$sParam.']';
- break;
-
- default:
- echo '[--'.$sParam.'=xxx]';
- break;
- }
- }
- echo "\n";
- exit(1);
+ if (!$bHelp) {
+ Utils::Log(LOG_ERR, "Unknown parameter(s): ".implode(' ', $aUnknownParameters));
+ }
+
+ echo "Usage:\n";
+ echo 'php '.basename($argv[0]);
+ foreach ($aOptionalParams as $sParam => $sType) {
+ switch ($sType) {
+ case 'boolean':
+ echo '[--'.$sParam.']';
+ break;
+
+ default:
+ echo '[--'.$sParam.'=xxx]';
+ break;
+ }
+ }
+ echo "\n";
+ exit(1);
}
$bResult = true;
@@ -76,59 +77,59 @@
$bDumpConfigOnly = (Utils::ReadBooleanParameter('dump_config_only', false) == true);
try {
- Utils::InitConsoleLogLevel();
- Utils::$iEventIssueLogLevel = Utils::ReadParameter('eventissue_log_level', Utils::GetConfigurationValue('eventissue_log_level', LOG_NONE));//On windows LOG_NOTICE=LOG_INFO=LOG_DEBUG=6
- $iMaxChunkSize = Utils::ReadParameter('max_chunk_size', Utils::GetConfigurationValue('max_chunk_size', 1000));
-
- if (file_exists(APPROOT.'collectors/main.php')) {
- require_once(APPROOT.'collectors/main.php');
- } else {
- Utils::Log(LOG_ERR, "The file '".APPROOT."collectors/main.php' is missing (or unreadable).");
- }
-
- if (!Orchestrator::CheckRequirements()) {
- exit(1);
- }
-
- $aConfig = Utils::GetConfigFiles();
- $sConfigDebug = "The following configuration files were loaded (in this order):\n\n";
- $idx = 1;
- foreach ($aConfig as $sFile) {
- $sConfigDebug .= "\t{$idx}. $sFile\n";
- $idx++;
- }
- $sConfigDebug .= "\nThe resulting configuration is:\n\n";
-
- $sConfigDebug .= Utils::DumpConfig();
-
- if ($bDumpConfigOnly) {
- echo $sConfigDebug;
- exit(0);
- } else {
- Utils::Log(LOG_DEBUG, $sConfigDebug);
- }
-
- $oOrchestrator = new Orchestrator();
- $aCollectors = $oOrchestrator->ListCollectors();
- Utils::Log(LOG_DEBUG, "Registered collectors:");
- foreach ($aCollectors as $oCollector) {
- Utils::Log(LOG_DEBUG, "Collector: ".$oCollector->GetName().", version: ".$oCollector->GetVersion());
- }
-
- if (!$bCollectOnly) {
- Utils::Log(LOG_DEBUG, 'iTop web services version: '.RestClient::GetNewestKnownVersion());
- $bResult = $oOrchestrator->InitSynchroDataSources($aCollectors);
- }
- if ($bResult && !$bSynchroOnly && !$bConfigureOnly) {
- $bResult = $oOrchestrator->Collect($aCollectors, $iMaxChunkSize, $bCollectOnly);
- }
-
- if ($bResult && !$bConfigureOnly && !$bCollectOnly) {
- $bResult = $oOrchestrator->Synchronize($aCollectors);
- }
+ Utils::InitConsoleLogLevel();
+ Utils::$iEventIssueLogLevel = Utils::ReadParameter('eventissue_log_level', Utils::GetConfigurationValue('eventissue_log_level', LOG_NONE));//On windows LOG_NOTICE=LOG_INFO=LOG_DEBUG=6
+ $iMaxChunkSize = Utils::ReadParameter('max_chunk_size', Utils::GetConfigurationValue('max_chunk_size', 1000));
+
+ if (file_exists(APPROOT.'collectors/main.php')) {
+ require_once(APPROOT.'collectors/main.php');
+ } else {
+ Utils::Log(LOG_ERR, "The file '".APPROOT."collectors/main.php' is missing (or unreadable).");
+ }
+
+ if (!Orchestrator::CheckRequirements()) {
+ exit(1);
+ }
+
+ $aConfig = Utils::GetConfigFiles();
+ $sConfigDebug = "The following configuration files were loaded (in this order):\n\n";
+ $idx = 1;
+ foreach ($aConfig as $sFile) {
+ $sConfigDebug .= "\t{$idx}. $sFile\n";
+ $idx++;
+ }
+ $sConfigDebug .= "\nThe resulting configuration is:\n\n";
+
+ $sConfigDebug .= Utils::DumpConfig();
+
+ if ($bDumpConfigOnly) {
+ echo $sConfigDebug;
+ exit(0);
+ } else {
+ Utils::Log(LOG_DEBUG, $sConfigDebug);
+ }
+
+ $oOrchestrator = new Orchestrator();
+ $aCollectors = $oOrchestrator->ListCollectors();
+ Utils::Log(LOG_DEBUG, "Registered collectors:");
+ foreach ($aCollectors as $oCollector) {
+ Utils::Log(LOG_DEBUG, "Collector: ".$oCollector->GetName().", version: ".$oCollector->GetVersion());
+ }
+
+ if (!$bCollectOnly) {
+ Utils::Log(LOG_DEBUG, 'iTop web services version: '.RestClient::GetNewestKnownVersion());
+ $bResult = $oOrchestrator->InitSynchroDataSources($aCollectors);
+ }
+ if ($bResult && !$bSynchroOnly && !$bConfigureOnly) {
+ $bResult = $oOrchestrator->Collect($aCollectors, $iMaxChunkSize, $bCollectOnly);
+ }
+
+ if ($bResult && !$bConfigureOnly && !$bCollectOnly) {
+ $bResult = $oOrchestrator->Synchronize($aCollectors);
+ }
} catch (Exception $e) {
- $bResult = false;
- Utils::Log(LOG_ERR, "Exception: ".$e->getMessage());
+ $bResult = false;
+ Utils::Log(LOG_ERR, "Exception: ".$e->getMessage());
}
-exit ($bResult ? 0 : 1); // exit code is zero means success
+exit($bResult ? 0 : 1); // exit code is zero means success
diff --git a/test/CallItopServiceTest.php b/test/CallItopServiceTest.php
index 11f809a..6067831 100644
--- a/test/CallItopServiceTest.php
+++ b/test/CallItopServiceTest.php
@@ -16,83 +16,84 @@
class CallItopServiceTest extends TestCase
{
- public function setUp(): void
- {
- parent::setUp();
- }
+ public function setUp(): void
+ {
+ parent::setUp();
+ }
- public function tearDown(): void
- {
- parent::tearDown();
- Utils::MockDoPostRequestService(null);
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ Utils::MockDoPostRequestService(null);
- $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
- $reflection->setAccessible(true);
- $reflection->setValue(null, null);
- }
+ $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
+ $reflection->setAccessible(true);
+ $reflection->setValue(null, null);
+ }
+ public function GetCredentialsProvider()
+ {
+ return [
+ 'login/password (nominal)' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2'
+ ],
+ 'aExpectedCredentials' => ['auth_user' => 'admin1', 'auth_pwd' => 'admin2']
+ ],
+ 'new token' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_token' => 'admin4',
+ ],
+ 'aExpectedCredentials' => ['auth_token' => 'admin4']
+ ],
+ 'new token over legacy one' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_rest_token' => 'admin3',
+ 'itop_token' => 'admin4',
+ ],
+ 'aExpectedCredentials' => ['auth_token' => 'admin4']
+ ],
+ ];
+ }
- public function GetCredentialsProvider(){
- return [
- 'login/password (nominal)' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2'
- ],
- 'aExpectedCredentials' => ['auth_user'=> 'admin1', 'auth_pwd'=>'admin2']
- ],
- 'new token' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_token' => 'admin4',
- ],
- 'aExpectedCredentials' => ['auth_token'=> 'admin4']
- ],
- 'new token over legacy one' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_rest_token' => 'admin3',
- 'itop_token' => 'admin4',
- ],
- 'aExpectedCredentials' => ['auth_token'=> 'admin4']
- ],
- ];
- }
+ /**
+ * @dataProvider GetCredentialsProvider
+ */
+ public function testCallItopViaHttp($aParameters, $aExpectedCredentials)
+ {
+ $oParametersMock = $this->createMock(\Parameters::class);
+ $oParametersMock->expects($this->atLeast(1))
+ ->method('Get')
+ ->will($this->returnCallback(
+ function ($sKey, $aDefaultValue) use ($aParameters) {
+ if (array_key_exists($sKey, $aParameters)) {
+ return $aParameters[$sKey];
+ }
+ return $aDefaultValue;
+ }
+ ));
- /**
- * @dataProvider GetCredentialsProvider
- */
- public function testCallItopViaHttp($aParameters, $aExpectedCredentials){
- $oParametersMock = $this->createMock(\Parameters::class);
- $oParametersMock->expects($this->atLeast(1))
- ->method('Get')
- ->will($this->returnCallback(
- function($sKey, $aDefaultValue) use ($aParameters) {
- if (array_key_exists($sKey, $aParameters)){
- return $aParameters[$sKey];
- }
- return $aDefaultValue;
- }
- ));
+ $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
+ $reflection->setAccessible(true);
+ $reflection->setValue(null, $oParametersMock);
- $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
- $reflection->setAccessible(true);
- $reflection->setValue(null, $oParametersMock);
+ $oMockedDoPostRequestService = $this->createMock(DoPostRequestService::class);
+ Utils::MockDoPostRequestService($oMockedDoPostRequestService);
- $oMockedDoPostRequestService = $this->createMock(DoPostRequestService::class);
- Utils::MockDoPostRequestService($oMockedDoPostRequestService);
+ $uri = 'http://itop.org';
+ $aAdditionalData = ['gabu' => 'zomeu'];
+ $oMockedDoPostRequestService->expects($this->once())
+ ->method('DoPostRequest')
+ ->with($uri, array_merge($aExpectedCredentials, $aAdditionalData))
+ ;
- $uri = 'http://itop.org';
- $aAdditionalData = ['gabu' => 'zomeu'];
- $oMockedDoPostRequestService->expects($this->once())
- ->method('DoPostRequest')
- ->with($uri, array_merge($aExpectedCredentials, $aAdditionalData ))
- ;
-
- $oCallItopService = new CallItopService();
- $oCallItopService->CallItopViaHttp($uri, $aAdditionalData);
- }
+ $oCallItopService = new CallItopService();
+ $oCallItopService->CallItopViaHttp($uri, $aAdditionalData);
+ }
}
diff --git a/test/CollectionPlanTest.php b/test/CollectionPlanTest.php
index 0a394b4..c6725b3 100644
--- a/test/CollectionPlanTest.php
+++ b/test/CollectionPlanTest.php
@@ -20,135 +20,135 @@
class CollectionPlanTest extends TestCase
{
- private static $sCollectorPath = APPROOT."/collectors/";
- private $oMockedLogger;
-
- public function setUp(): void
- {
- parent::setUp();
-
- $this->CleanCollectorsFiles();
- }
-
- public function tearDown(): void
- {
- parent::tearDown();
- $this->CleanCollectorsFiles();
- }
-
- /**
- * @return void
- * @throws \IOException
- */
- public function testGetSortedLaunchSequence()
- {
- $this->CopyCollectorsFiles();
-
- require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
- $oTestCollectionPlan = new TestCollectionPlan();
- $oTestCollectionPlan->Init();
-
- $aCollectorsLaunchSequence = $oTestCollectionPlan->GetSortedLaunchSequence();
-
- foreach ($aCollectorsLaunchSequence as $iKey => $aCollector) {
- $this->assertFalse($aCollector['name'] == 'StandardCollectorWithNoRank');
- }
- $this->assertTrue($aCollectorsLaunchSequence[1]['name'] == 'ExtendedCollector');
- }
-
- /**
- * @return void
- * @throws \IOException
- */
- public function testGetCollectorDefinitionFile()
- {
- $this->CopyCollectorsFiles();
-
- require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
- $oTestCollectionPlan = new TestCollectionPlan();
- $oTestCollectionPlan->Init();
-
- $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('ExtendedCollector'));
- $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('StandardCollector'));
- $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('LegacyCollector'));
- $this->assertFalse($oTestCollectionPlan->GetCollectorDefinitionFile('OtherCollector'));
- }
-
- /**
- * @return void
- * @throws \IOException
- */
- public function testAddCollectorsToOrchestrator()
- {
- $aCollector = ['ExtendedCollector', 'StandardCollector', 'LegacyCollector', 'OtherCollector'];
-
- $this->CopyCollectorsFiles();
-
- require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
- $oTestCollectionPlan = new TestCollectionPlan();
- $oTestCollectionPlan->Init();
- $oTestCollectionPlan->AddCollectorsToOrchestrator();
-
- $oOrchestrator = new Orchestrator();
- $aOrchestratedCollectors = $oOrchestrator->ListCollectors();
-
- $this->assertArrayHasKey(0, $aOrchestratedCollectors);
- $this->assertTrue(($aOrchestratedCollectors[0] instanceof StandardCollector) || ($aOrchestratedCollectors[0] instanceof ExtendedCollector));
- $this->assertArrayHasKey(1, $aOrchestratedCollectors);
- $this->assertTrue(($aOrchestratedCollectors[1] instanceof StandardCollector) || ($aOrchestratedCollectors[1] instanceof ExtendedCollector));
- }
-
- private function CopyCollectorsFiles()
- {
- $aPatterns = [
- '' => '/test/collectionplan/collectors_files/*',
- 'extensions/' => '/test/collectionplan/collectors_files/extensions/*',
- 'extensions/src/' => '/test/collectionplan/collectors_files/extensions/src/*',
- 'extensions/json/' => '/test/collectionplan/collectors_files/extensions/json/*',
- 'src/' => '/test/collectionplan/collectors_files/src/*',
- 'json/' => '/test/collectionplan/collectors_files/json/*',
- ];
- foreach ($aPatterns as $sDir => $sPattern) {
- if (!is_dir(self::$sCollectorPath.$sDir)) {
- mkdir(self::$sCollectorPath.$sDir);
- }
- $this->CopyFile($sDir, APPROOT.$sPattern);
- }
- }
-
- private function CopyFile($sDir, $sPattern)
- {
- $aFiles = glob($sPattern);
- foreach ($aFiles as $sFile) {
- if (is_file($sFile)) {
- $bRes = copy($sFile, self::$sCollectorPath.'/'.$sDir.basename($sFile));
- if (!$bRes) {
- throw new Exception("Failed copying $sFile to ".self::$sCollectorPath.'/'.$sDir.basename($sFile));
- }
- }
- }
- }
-
- private function CleanCollectorsFiles()
- {
- $aPatterns = [
- 'extensions/src/' => 'extensions/src/*',
- 'extensions/json/' => 'extensions/json/*',
- 'extensions/' => 'extensions/*',
- 'src/' => 'src/*',
- 'json/' => 'json/*',
- '' => '*',
- ];
- foreach ($aPatterns as $sDir => $sPattern) {
- $aCollectorFiles = glob(self::$sCollectorPath.$sPattern);
- foreach ($aCollectorFiles as $sFile) {
- unlink($sFile);
- }
-
- if (is_dir(self::$sCollectorPath.$sDir)) {
- rmdir(self::$sCollectorPath.$sDir);
- }
- }
- }
+ private static $sCollectorPath = APPROOT."/collectors/";
+ private $oMockedLogger;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->CleanCollectorsFiles();
+ }
+
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ $this->CleanCollectorsFiles();
+ }
+
+ /**
+ * @return void
+ * @throws \IOException
+ */
+ public function testGetSortedLaunchSequence()
+ {
+ $this->CopyCollectorsFiles();
+
+ require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
+ $oTestCollectionPlan = new TestCollectionPlan();
+ $oTestCollectionPlan->Init();
+
+ $aCollectorsLaunchSequence = $oTestCollectionPlan->GetSortedLaunchSequence();
+
+ foreach ($aCollectorsLaunchSequence as $iKey => $aCollector) {
+ $this->assertFalse($aCollector['name'] == 'StandardCollectorWithNoRank');
+ }
+ $this->assertTrue($aCollectorsLaunchSequence[1]['name'] == 'ExtendedCollector');
+ }
+
+ /**
+ * @return void
+ * @throws \IOException
+ */
+ public function testGetCollectorDefinitionFile()
+ {
+ $this->CopyCollectorsFiles();
+
+ require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
+ $oTestCollectionPlan = new TestCollectionPlan();
+ $oTestCollectionPlan->Init();
+
+ $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('ExtendedCollector'));
+ $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('StandardCollector'));
+ $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('LegacyCollector'));
+ $this->assertFalse($oTestCollectionPlan->GetCollectorDefinitionFile('OtherCollector'));
+ }
+
+ /**
+ * @return void
+ * @throws \IOException
+ */
+ public function testAddCollectorsToOrchestrator()
+ {
+ $aCollector = ['ExtendedCollector', 'StandardCollector', 'LegacyCollector', 'OtherCollector'];
+
+ $this->CopyCollectorsFiles();
+
+ require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
+ $oTestCollectionPlan = new TestCollectionPlan();
+ $oTestCollectionPlan->Init();
+ $oTestCollectionPlan->AddCollectorsToOrchestrator();
+
+ $oOrchestrator = new Orchestrator();
+ $aOrchestratedCollectors = $oOrchestrator->ListCollectors();
+
+ $this->assertArrayHasKey(0, $aOrchestratedCollectors);
+ $this->assertTrue(($aOrchestratedCollectors[0] instanceof StandardCollector) || ($aOrchestratedCollectors[0] instanceof ExtendedCollector));
+ $this->assertArrayHasKey(1, $aOrchestratedCollectors);
+ $this->assertTrue(($aOrchestratedCollectors[1] instanceof StandardCollector) || ($aOrchestratedCollectors[1] instanceof ExtendedCollector));
+ }
+
+ private function CopyCollectorsFiles()
+ {
+ $aPatterns = [
+ '' => '/test/collectionplan/collectors_files/*',
+ 'extensions/' => '/test/collectionplan/collectors_files/extensions/*',
+ 'extensions/src/' => '/test/collectionplan/collectors_files/extensions/src/*',
+ 'extensions/json/' => '/test/collectionplan/collectors_files/extensions/json/*',
+ 'src/' => '/test/collectionplan/collectors_files/src/*',
+ 'json/' => '/test/collectionplan/collectors_files/json/*',
+ ];
+ foreach ($aPatterns as $sDir => $sPattern) {
+ if (!is_dir(self::$sCollectorPath.$sDir)) {
+ mkdir(self::$sCollectorPath.$sDir);
+ }
+ $this->CopyFile($sDir, APPROOT.$sPattern);
+ }
+ }
+
+ private function CopyFile($sDir, $sPattern)
+ {
+ $aFiles = glob($sPattern);
+ foreach ($aFiles as $sFile) {
+ if (is_file($sFile)) {
+ $bRes = copy($sFile, self::$sCollectorPath.'/'.$sDir.basename($sFile));
+ if (!$bRes) {
+ throw new Exception("Failed copying $sFile to ".self::$sCollectorPath.'/'.$sDir.basename($sFile));
+ }
+ }
+ }
+ }
+
+ private function CleanCollectorsFiles()
+ {
+ $aPatterns = [
+ 'extensions/src/' => 'extensions/src/*',
+ 'extensions/json/' => 'extensions/json/*',
+ 'extensions/' => 'extensions/*',
+ 'src/' => 'src/*',
+ 'json/' => 'json/*',
+ '' => '*',
+ ];
+ foreach ($aPatterns as $sDir => $sPattern) {
+ $aCollectorFiles = glob(self::$sCollectorPath.$sPattern);
+ foreach ($aCollectorFiles as $sFile) {
+ unlink($sFile);
+ }
+
+ if (is_dir(self::$sCollectorPath.$sDir)) {
+ rmdir(self::$sCollectorPath.$sDir);
+ }
+ }
+ }
}
diff --git a/test/CollectorSynchroTest.php b/test/CollectorSynchroTest.php
index 148fa21..418a13a 100644
--- a/test/CollectorSynchroTest.php
+++ b/test/CollectorSynchroTest.php
@@ -17,14 +17,15 @@
class CollectorSynchroTest extends TestCase
{
- private $oMockedCallItopService;
+ private $oMockedCallItopService;
- public function SynchroOutputProvider(){
- $sRetcodeOutput = <<
ERROR: All records have been untouched for some time (all of the objects could be deleted). Please check that the process that writes into the synchronization table is still running. Operation cancelled.
Replicas: 14
Replicas touched since last synchro: 0
Objects deleted: 0
Objects deletion errors: 0
Objects obsoleted: 0
Objects obsolescence errors: 0
Objects created: 0 (0 warnings)
Objects creation errors: 0
Objects updated: 0 (0 warnings)
Objects update errors: 0
Objects reconciled (updated): 0 (0 warnings)
Objects reconciled (unchanged): 0 (0 warnings)
Objects reconciliation errors: 0
Replica disappeared, no action taken: 0
TXT;
- $sFailedNoMatch = <<Working on Synchro LDAP Person (id=3)...
Objects reconciliation errors: 5\n",
- ],
- 'records have been untouched' => [
- 'sOutput' => $sFailedOutputWithNoErrorCount,
- 'iExpectedErrorCount' => 1,
- 'sErrorMessage' => "All records have been untouched for some time (all of the objects could be deleted). Please check that the process that writes into the synchronization table is still running. Operation cancelled\n",
- ],
- 'synchro ok' => [
- 'sOutput' => $sMsgOK,
- 'iExpectedErrorCount' => 0,
- 'sErrorMessage' => "",
- ],
- ];
- }
-
- /**
- * @dataProvider ParseSynchroExecOutput
- */
- public function testParseSynchroExecOutput($sOutput, $iExpectedErrorCount, $sErrorMessage=null){
- $oCollector = new \FakeCollector();
- $this->assertEquals($iExpectedErrorCount, $oCollector->ParseSynchroExecOutput($sOutput), $sOutput);
- $this->assertEquals($sErrorMessage, $oCollector->GetErrorMessage(), $sOutput);
- }
+ return [
+ 'login failed' => [
+ 'sOutput' => 'eeeee 1,
+ 'sErrorMessage' => "Failed to login to iTop. Invalid (or insufficent) credentials.\n",
+ ],
+ 'weird output' => [
+ 'sOutput' => $sFailedNoMatch,
+ 'iExpectedErrorCount' => 1,
+ 'sErrorMessage' => "NOMATCH",
+ ],
+ 'synchro error count parsing' => [
+ 'sOutput' => $sFailedOutput,
+ 'iExpectedErrorCount' => 5,
+ 'sErrorMessage' => "Objects deleted: 0
Objects deletion errors: 1
Objects obsoleted: 0
Objects obsolescence errors: 2
Objects created: 0 (0 warnings)
Objects creation errors: 3
Objects updated: 0 (0 warnings)
Objects update errors: 4
Objects reconciled (updated): 0 (0 warnings)
Objects reconciled (unchanged): 0 (0 warnings)
Objects reconciliation errors: 5\n",
+ ],
+ 'records have been untouched' => [
+ 'sOutput' => $sFailedOutputWithNoErrorCount,
+ 'iExpectedErrorCount' => 1,
+ 'sErrorMessage' => "All records have been untouched for some time (all of the objects could be deleted). Please check that the process that writes into the synchronization table is still running. Operation cancelled\n",
+ ],
+ 'synchro ok' => [
+ 'sOutput' => $sMsgOK,
+ 'iExpectedErrorCount' => 0,
+ 'sErrorMessage' => "",
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider ParseSynchroExecOutput
+ */
+ public function testParseSynchroExecOutput($sOutput, $iExpectedErrorCount, $sErrorMessage = null)
+ {
+ $oCollector = new \FakeCollector();
+ $this->assertEquals($iExpectedErrorCount, $oCollector->ParseSynchroExecOutput($sOutput), $sOutput);
+ $this->assertEquals($sErrorMessage, $oCollector->GetErrorMessage(), $sOutput);
+ }
}
diff --git a/test/CollectorTest.php b/test/CollectorTest.php
index 67004b5..d2fd7ab 100644
--- a/test/CollectorTest.php
+++ b/test/CollectorTest.php
@@ -17,46 +17,46 @@
class CollectorTest extends TestCase
{
- private static $sCollectorPath = APPROOT."/collectors/";
+ private static $sCollectorPath = APPROOT."/collectors/";
- public function setUp(): void
- {
- parent::setUp();
+ public function setUp(): void
+ {
+ parent::setUp();
- $aCollectorFiles = glob(self::$sCollectorPath."*");
- foreach ($aCollectorFiles as $sFile) {
- unlink($sFile);
- }
+ $aCollectorFiles = glob(self::$sCollectorPath."*");
+ foreach ($aCollectorFiles as $sFile) {
+ unlink($sFile);
+ }
// Remove params.local.xml if it exists because it can interfere with the tests
if (file_exists(APPROOT."/conf/params.local.xml")) {
unlink(APPROOT."/conf/params.local.xml");
}
- }
+ }
public function tearDown(): void
- {
- parent::tearDown();
- $aCollectorFiles = glob(self::$sCollectorPath."*");
- foreach ($aCollectorFiles as $sFile) {
- unlink($sFile);
- }
- }
+ {
+ parent::tearDown();
+ $aCollectorFiles = glob(self::$sCollectorPath."*");
+ foreach ($aCollectorFiles as $sFile) {
+ unlink($sFile);
+ }
+ }
- public function AttributeIsNullifiedProvider()
- {
- $sEmptyConfig = '';
- $sOtherAttributeNulllified = <<';
+ $sOtherAttributeNulllified = <<first_name
XML;
- $sPhoneNullifiedSection = <<phone
XML;
- $sPhoneNullifiedSubSection = <<phone
@@ -64,23 +64,23 @@ public function AttributeIsNullifiedProvider()
XML;
- return [
- 'no nullify config' => [''],
- 'empty nullify config' => [$sEmptyConfig],
- 'other attributes nullified' => [$sOtherAttributeNulllified],
- 'phone nullified in iTopPersonCollector_nullified_attributes section' => [$sPhoneNullifiedSection, true],
- 'phone nullified in nullified_attributes sub section' => [$sPhoneNullifiedSubSection, true],
- ];
- }
+ return [
+ 'no nullify config' => [''],
+ 'empty nullify config' => [$sEmptyConfig],
+ 'other attributes nullified' => [$sOtherAttributeNulllified],
+ 'phone nullified in iTopPersonCollector_nullified_attributes section' => [$sPhoneNullifiedSection, true],
+ 'phone nullified in nullified_attributes sub section' => [$sPhoneNullifiedSubSection, true],
+ ];
+ }
- /**
- * @dataProvider AttributeIsNullifiedProvider
- */
- public function testAttributeIsNullified($sCollectorXmlSubSection, $bExpectedIsNullified = false)
- {
- $this->copy(APPROOT."/test/collector/attribute_isnullified/*");
+ /**
+ * @dataProvider AttributeIsNullifiedProvider
+ */
+ public function testAttributeIsNullified($sCollectorXmlSubSection, $bExpectedIsNullified = false)
+ {
+ $this->copy(APPROOT."/test/collector/attribute_isnullified/*");
- $sXml = <<
@@ -100,178 +100,178 @@ public function testAttributeIsNullified($sCollectorXmlSubSection, $bExpectedIsN
XML;
- file_put_contents(self::$sCollectorPath."params.distrib.xml", $sXml);
+ file_put_contents(self::$sCollectorPath."params.distrib.xml", $sXml);
- require_once self::$sCollectorPath."iTopPersonCollector.class.inc.php";
- Utils::LoadConfig();
- $oCollector = new iTopPersonCollector();
- $oCollector->Init();
+ require_once self::$sCollectorPath."iTopPersonCollector.class.inc.php";
+ Utils::LoadConfig();
+ $oCollector = new iTopPersonCollector();
+ $oCollector->Init();
- $this->assertEquals($bExpectedIsNullified, $oCollector->AttributeIsNullified('phone'));
+ $this->assertEquals($bExpectedIsNullified, $oCollector->AttributeIsNullified('phone'));
- $oCollector->Collect(1);
- $sExpectedCsv = <<Collect(1);
+ $sExpectedCsv = <<;123456;9998877665544;issac.asimov@function.io;writer
CSV;
- if ($bExpectedIsNullified) {
- $this->assertEquals($sExpectedNullifiedCsv, file_get_contents(APPROOT."/data/iTopPersonCollector-1.csv"));
- } else {
- $this->assertEquals($sExpectedCsv, file_get_contents(APPROOT."/data/iTopPersonCollector-1.csv"));
- }
- }
+ if ($bExpectedIsNullified) {
+ $this->assertEquals($sExpectedNullifiedCsv, file_get_contents(APPROOT."/data/iTopPersonCollector-1.csv"));
+ } else {
+ $this->assertEquals($sExpectedCsv, file_get_contents(APPROOT."/data/iTopPersonCollector-1.csv"));
+ }
+ }
+
+ protected function copy($sPattern)
+ {
+ if (!is_dir(self::$sCollectorPath)) {
+ mkdir(self::$sCollectorPath);
+ }
+
+ $aFiles = glob($sPattern);
+ foreach ($aFiles as $sFile) {
+ if (is_file($sFile)) {
+ if (!copy($sFile, self::$sCollectorPath.basename($sFile))) {
+ throw new Exception("Failed copying $sFile to ".self::$sCollectorPath.basename($sFile));
+ }
+ }
+ }
+ }
+
+ /**
+ * @dataProvider providerUpdateSDSAttributes
+ * @param array $aExpectedAttrDef
+ * @param array $aSynchroAttrDef
+ * @param bool $bWillUpdate
+ */
+ public function testUpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttrDef, bool $bWillUpdate)
+ {
+ $this->copy(APPROOT."/test/collector/attribute_isnullified/*");
+ require_once APPROOT."/core/restclient.class.inc.php";
+ require_once self::$sCollectorPath."iTopPersonCollector.class.inc.php";
+ $oCollector = new iTopPersonCollector();
+ $oMockClient = $this->CreateMock('RestClient');
+ $oMockClient->expects($this->exactly($bWillUpdate ? 1 : 0))->method("Update")->willReturn(['code' => 0]);
+
+ $bRet = $this->InvokeNonPublicMethod(get_class($oCollector), 'UpdateSDSAttributes', $oCollector, [$aExpectedAttrDef, $aSynchroAttrDef, '', $oMockClient]);
- protected function copy($sPattern)
- {
- if (!is_dir(self::$sCollectorPath)) {
- mkdir(self::$sCollectorPath);
- }
+ $this->assertTrue($bRet);
+ }
- $aFiles = glob($sPattern);
- foreach ($aFiles as $sFile) {
- if (is_file($sFile)) {
- if (!copy($sFile, self::$sCollectorPath.basename($sFile))) {
- throw new Exception("Failed copying $sFile to ".self::$sCollectorPath.basename($sFile));
- }
- }
- }
- }
+ public function providerUpdateSDSAttributes()
+ {
+ return [
+ 'no difference' => [
+ 'aExpectedAttrDef' => [
+ [
+ "attcode" => "name",
+ "update" => "1",
+ "reconcile" => "1",
+ "update_policy" => "master_locked",
+ "finalclass" => "SynchroAttribute",
+ "friendlyname" => "name",
+ ],
+ ],
+ 'aSynchroAttrDef' => [
+ [
+ "attcode" => "name",
+ "update" => "1",
+ "reconcile" => "1",
+ "update_policy" => "master_locked",
+ "finalclass" => "SynchroAttribute",
+ "friendlyname" => "name",
+ ],
+ ],
+ 'bWillUpdate' => false,
+ ],
+ 'reconcile is different' => [
+ 'aExpectedAttrDef' => [
+ [
+ "attcode" => "name",
+ "update" => "1",
+ "reconcile" => "1",
+ "update_policy" => "master_locked",
+ "finalclass" => "SynchroAttribute",
+ "friendlyname" => "name",
+ ],
+ ],
+ 'aSynchroAttrDef' => [
+ [
+ "attcode" => "name",
+ "update" => "1",
+ "reconcile" => "0", // Difference here
+ "update_policy" => "master_locked",
+ "finalclass" => "SynchroAttribute",
+ "friendlyname" => "name",
+ ],
+ ],
+ 'bWillUpdate' => true,
+ ],
+ 'update policy is different on OPTIONAL field' => [
+ 'aExpectedAttrDef' => [
+ [
+ "attcode" => "optional", // Note: 'optional' is an attribute considered as "optional" in this test
+ "update" => "1",
+ "reconcile" => "1",
+ "update_policy" => "master_locked",
+ "finalclass" => "SynchroAttribute",
+ "friendlyname" => "optional",
+ ],
+ ],
+ 'aSynchroAttrDef' => [
+ [
+ "attcode" => "optional",
+ "update" => "1",
+ "reconcile" => "1",
+ "update_policy" => "master_unlocked", // Difference here
+ "finalclass" => "SynchroAttribute",
+ "friendlyname" => "optional",
+ ],
+ ],
+ 'bWillUpdate' => true,
+ ],
+ 'OPTIONAL field actually missing' => [
+ 'aExpectedAttrDef' => [
+ ],
+ 'aSynchroAttrDef' => [
+ [
+ "attcode" => "optional",
+ "update" => "1",
+ "reconcile" => "1",
+ "update_policy" => "master_unlocked", // Difference here
+ "finalclass" => "SynchroAttribute",
+ "friendlyname" => "optional",
+ ],
+ ],
+ 'bWillUpdate' => false,
+ ],
+ ];
+ }
- /**
- * @dataProvider providerUpdateSDSAttributes
- * @param array $aExpectedAttrDef
- * @param array $aSynchroAttrDef
- * @param bool $bWillUpdate
- */
- public function testUpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttrDef, bool $bWillUpdate)
- {
- $this->copy(APPROOT."/test/collector/attribute_isnullified/*");
- require_once APPROOT."/core/restclient.class.inc.php";
- require_once self::$sCollectorPath."iTopPersonCollector.class.inc.php";
- $oCollector = new iTopPersonCollector();
- $oMockClient = $this->CreateMock('RestClient');
- $oMockClient->expects($this->exactly($bWillUpdate ? 1 : 0))->method("Update")->willReturn(['code' => 0]);
-
- $bRet = $this->InvokeNonPublicMethod(get_class($oCollector), 'UpdateSDSAttributes', $oCollector, [$aExpectedAttrDef, $aSynchroAttrDef, '', $oMockClient]);
-
- $this->assertTrue($bRet);
- }
-
- public function providerUpdateSDSAttributes()
- {
- return [
- 'no difference' => [
- 'aExpectedAttrDef' => [
- [
- "attcode" => "name",
- "update" => "1",
- "reconcile" => "1",
- "update_policy" => "master_locked",
- "finalclass" => "SynchroAttribute",
- "friendlyname" => "name",
- ],
- ],
- 'aSynchroAttrDef' => [
- [
- "attcode" => "name",
- "update" => "1",
- "reconcile" => "1",
- "update_policy" => "master_locked",
- "finalclass" => "SynchroAttribute",
- "friendlyname" => "name",
- ],
- ],
- 'bWillUpdate' => false,
- ],
- 'reconcile is different' => [
- 'aExpectedAttrDef' => [
- [
- "attcode" => "name",
- "update" => "1",
- "reconcile" => "1",
- "update_policy" => "master_locked",
- "finalclass" => "SynchroAttribute",
- "friendlyname" => "name",
- ],
- ],
- 'aSynchroAttrDef' => [
- [
- "attcode" => "name",
- "update" => "1",
- "reconcile" => "0", // Difference here
- "update_policy" => "master_locked",
- "finalclass" => "SynchroAttribute",
- "friendlyname" => "name",
- ],
- ],
- 'bWillUpdate' => true,
- ],
- 'update policy is different on OPTIONAL field' => [
- 'aExpectedAttrDef' => [
- [
- "attcode" => "optional", // Note: 'optional' is an attribute considered as "optional" in this test
- "update" => "1",
- "reconcile" => "1",
- "update_policy" => "master_locked",
- "finalclass" => "SynchroAttribute",
- "friendlyname" => "optional",
- ],
- ],
- 'aSynchroAttrDef' => [
- [
- "attcode" => "optional",
- "update" => "1",
- "reconcile" => "1",
- "update_policy" => "master_unlocked", // Difference here
- "finalclass" => "SynchroAttribute",
- "friendlyname" => "optional",
- ],
- ],
- 'bWillUpdate' => true,
- ],
- 'OPTIONAL field actually missing' => [
- 'aExpectedAttrDef' => [
- ],
- 'aSynchroAttrDef' => [
- [
- "attcode" => "optional",
- "update" => "1",
- "reconcile" => "1",
- "update_policy" => "master_unlocked", // Difference here
- "finalclass" => "SynchroAttribute",
- "friendlyname" => "optional",
- ],
- ],
- 'bWillUpdate' => false,
- ],
- ];
- }
+ /**
+ * @param string $sObjectClass for example DBObject::class
+ * @param string $sMethodName
+ * @param object $oObject
+ * @param array $aArgs
+ *
+ * @return mixed method result
+ *
+ * @throws \ReflectionException
+ *
+ * @since 2.7.4 3.0.0
+ */
+ public function InvokeNonPublicMethod($sObjectClass, $sMethodName, $oObject, $aArgs)
+ {
+ $class = new \ReflectionClass($sObjectClass);
+ $method = $class->getMethod($sMethodName);
+ $method->setAccessible(true);
- /**
- * @param string $sObjectClass for example DBObject::class
- * @param string $sMethodName
- * @param object $oObject
- * @param array $aArgs
- *
- * @return mixed method result
- *
- * @throws \ReflectionException
- *
- * @since 2.7.4 3.0.0
- */
- public function InvokeNonPublicMethod($sObjectClass, $sMethodName, $oObject, $aArgs)
- {
- $class = new \ReflectionClass($sObjectClass);
- $method = $class->getMethod($sMethodName);
- $method->setAccessible(true);
-
- return $method->invokeArgs($oObject, $aArgs);
- }
+ return $method->invokeArgs($oObject, $aArgs);
+ }
}
diff --git a/test/CsvCollectorTest.php b/test/CsvCollectorTest.php
index 9de8f71..93afe17 100644
--- a/test/CsvCollectorTest.php
+++ b/test/CsvCollectorTest.php
@@ -20,65 +20,65 @@
class CsvCollectorTest extends CollectorTest
{
- private static $sCollectorPath = APPROOT."/collectors/";
- private $oMockedLogger;
+ private static $sCollectorPath = APPROOT."/collectors/";
+ private $oMockedLogger;
- public function setUp(): void
- {
- parent::setUp();
+ public function setUp(): void
+ {
+ parent::setUp();
- $this->oMockedLogger = $this->createMock("UtilsLogger");
- Utils::MockLog($this->oMockedLogger);
- }
+ $this->oMockedLogger = $this->createMock("UtilsLogger");
+ Utils::MockLog($this->oMockedLogger);
+ }
- public function testIOException()
- {
- $this->assertFalse(is_a(new Exception(""), "IOException"));
- $this->assertTrue(is_a(new IOException(""), "IOException"));
- }
+ public function testIOException()
+ {
+ $this->assertFalse(is_a(new Exception(""), "IOException"));
+ $this->assertTrue(is_a(new IOException(""), "IOException"));
+ }
- /**
- * @param bool $sAdditionalDir
+ /**
+ * @param bool $sAdditionalDir
* Note: The order of fields in Expected_generated.csv does not matter -> it is not tested
- *
- * @dataProvider OrgCollectorProvider
- * @throws \Exception
- */
- public function testOrgCollector($sAdditionalDir = false)
- {
- $this->copy(APPROOT."/test/single_csv/common/*");
- $this->copy(APPROOT."/test/single_csv/".$sAdditionalDir."/*");
-
- require_once self::$sCollectorPath."iTopPersonCsvCollector.class.inc.php";
-
- $this->oMockedLogger->expects($this->exactly(0))
- ->method("Log");
-
- $oOrgCollector = new iTopPersonCsvCollector();
- Utils::LoadConfig();
- $oOrgCollector->Init();
-
- $this->assertTrue($oOrgCollector->Collect());
-
- $sExpected_content = file_get_contents(self::$sCollectorPath."expected_generated.csv");
-
- $this->assertEquals($sExpected_content, file_get_contents(APPROOT."/data/iTopPersonCsvCollector-1.csv"));
- }
-
- public function OrgCollectorProvider()
- {
- return [
- "nominal" => ["nominal"],
- "charset_ISO" => ["charset_ISO"],
- "separator" => ["separator"],
- "separator_tab" => ["separator_tab"],
- "clicommand" => ["clicommand"],
- "adding hardcoded values" => ["hardcoded_values_add"],
- "replacing hardcoded values" => ["hardcoded_values_replace"],
- "ignored attributes" => ["ignored_attributes"],
- "configured header" => ["configured_header"],
- "mapping" => ["mapping"],
- "separator_incolumns" => ["separator_incolumns"],
+ *
+ * @dataProvider OrgCollectorProvider
+ * @throws \Exception
+ */
+ public function testOrgCollector($sAdditionalDir = false)
+ {
+ $this->copy(APPROOT."/test/single_csv/common/*");
+ $this->copy(APPROOT."/test/single_csv/".$sAdditionalDir."/*");
+
+ require_once self::$sCollectorPath."iTopPersonCsvCollector.class.inc.php";
+
+ $this->oMockedLogger->expects($this->exactly(0))
+ ->method("Log");
+
+ $oOrgCollector = new iTopPersonCsvCollector();
+ Utils::LoadConfig();
+ $oOrgCollector->Init();
+
+ $this->assertTrue($oOrgCollector->Collect());
+
+ $sExpected_content = file_get_contents(self::$sCollectorPath."expected_generated.csv");
+
+ $this->assertEquals($sExpected_content, file_get_contents(APPROOT."/data/iTopPersonCsvCollector-1.csv"));
+ }
+
+ public function OrgCollectorProvider()
+ {
+ return [
+ "nominal" => ["nominal"],
+ "charset_ISO" => ["charset_ISO"],
+ "separator" => ["separator"],
+ "separator_tab" => ["separator_tab"],
+ "clicommand" => ["clicommand"],
+ "adding hardcoded values" => ["hardcoded_values_add"],
+ "replacing hardcoded values" => ["hardcoded_values_replace"],
+ "ignored attributes" => ["ignored_attributes"],
+ "configured header" => ["configured_header"],
+ "mapping" => ["mapping"],
+ "separator_incolumns" => ["separator_incolumns"],
"mapping 1 column twice" => ["map_1_column_twice"],
"mapping 1 column twice adding primary key" => ["map_1_column_twice_primary_key"],
"mapping 1 column twice without header" => ["map_1_column_twice_without_header"],
@@ -87,131 +87,131 @@ public function OrgCollectorProvider()
"mapping 2 columns 3 times" => ["map_2_columns_3_times"],
"return_in_fieldvalues" => ["return_in_fieldvalues"],
"multicolumns_attachment" => ["multicolumns_attachment"],
- ];
- }
-
- public function testAbsolutePath()
- {
- $this->copy(APPROOT."/test/single_csv/common/*");
- $sTargetDir = tempnam(sys_get_temp_dir(), 'build-');
- @unlink($sTargetDir);
- mkdir($sTargetDir);
- $sTargetDir = realpath($sTargetDir);
- $sContent = str_replace("TMPDIR", $sTargetDir, file_get_contents(APPROOT."/test/single_csv/absolutepath/params.distrib.xml"));
- $rHandle = fopen(APPROOT."/collectors/params.distrib.xml", "w");
- fwrite($rHandle, $sContent);
- fclose($rHandle);
-
- $sCsvFile = dirname(__FILE__)."/single_csv/nominal/iTopPersonCsvCollector.csv";
- if (is_file($sCsvFile)) {
- copy($sCsvFile, $sTargetDir."/iTopPersonCsvCollector.csv");
- } else {
- throw new \Exception("Cannot find $sCsvFile file");
- }
-
- require_once self::$sCollectorPath."iTopPersonCsvCollector.class.inc.php";
-
- $this->oMockedLogger->expects($this->exactly(0))
- ->method("Log");
-
- $oOrgCollector = new iTopPersonCsvCollector();
- Utils::LoadConfig();
- $oOrgCollector->Init();
-
- $this->assertTrue($oOrgCollector->Collect());
-
- $sExpected_content = file_get_contents(dirname(__FILE__)."/single_csv/nominal/expected_generated.csv");
-
- $this->assertEquals($sExpected_content, file_get_contents(APPROOT."/data/iTopPersonCsvCollector-1.csv"));
- }
-
- /**
- * @param $error_file
- * @param $error_msg
- * @param bool $exception_msg
- *
- * @throws \Exception
- * @dataProvider ErrorFileProvider
- */
- public function testCsvErrors($sErrorFile, $sErrorMsg, $sExceptionMsg = false)
- {
- $this->copy(APPROOT."/test/single_csv/common/*");
- copy(APPROOT."/test/single_csv/csv_errors/$sErrorFile", self::$sCollectorPath."iTopPersonCsvCollector.csv");
-
- require_once self::$sCollectorPath."iTopPersonCsvCollector.class.inc.php";
- $orgCollector = new iTopPersonCsvCollector();
- Utils::LoadConfig();
- $orgCollector->Init();
-
- if ($sExceptionMsg) {
- $this->oMockedLogger->expects($this->exactly(2))
- ->method("Log")
- ->withConsecutive(array(LOG_ERR, $sErrorMsg), array(LOG_ERR, $sExceptionMsg));
- } else {
- if ($sErrorMsg) {
- $this->oMockedLogger->expects($this->exactly(1))
- ->method("Log");
- } else {
- $this->oMockedLogger->expects($this->exactly(0))
- ->method("Log");
- }
- }
- try {
- $bResult = $orgCollector->Collect();
-
- $this->assertEquals($sExceptionMsg ? false : true, $bResult);
- } catch (Exception $e) {
- $this->assertEquals($sExceptionMsg, $e->getMessage());
- }
- }
-
- public function ErrorFileProvider()
- {
- return array(
- "wrong number of line" => array(
- "wrongnumber_columns_inaline.csv",
- "[iTopPersonCsvCollector] Wrong number of columns (1) on line 2 (expected 18 columns just like in header): aa",
- 'iTopPersonCsvCollector::Collect() got an exception: Invalid CSV file.',
- ),
- "no primary key" => array(
- "no_primarykey.csv",
- "[iTopPersonCsvCollector] The mandatory column \"primary_key\" is missing in the csv file.",
- 'iTopPersonCsvCollector::Collect() got an exception: Missing columns in the csv file.',
- ),
- "no email" => array(
+ ];
+ }
+
+ public function testAbsolutePath()
+ {
+ $this->copy(APPROOT."/test/single_csv/common/*");
+ $sTargetDir = tempnam(sys_get_temp_dir(), 'build-');
+ @unlink($sTargetDir);
+ mkdir($sTargetDir);
+ $sTargetDir = realpath($sTargetDir);
+ $sContent = str_replace("TMPDIR", $sTargetDir, file_get_contents(APPROOT."/test/single_csv/absolutepath/params.distrib.xml"));
+ $rHandle = fopen(APPROOT."/collectors/params.distrib.xml", "w");
+ fwrite($rHandle, $sContent);
+ fclose($rHandle);
+
+ $sCsvFile = dirname(__FILE__)."/single_csv/nominal/iTopPersonCsvCollector.csv";
+ if (is_file($sCsvFile)) {
+ copy($sCsvFile, $sTargetDir."/iTopPersonCsvCollector.csv");
+ } else {
+ throw new \Exception("Cannot find $sCsvFile file");
+ }
+
+ require_once self::$sCollectorPath."iTopPersonCsvCollector.class.inc.php";
+
+ $this->oMockedLogger->expects($this->exactly(0))
+ ->method("Log");
+
+ $oOrgCollector = new iTopPersonCsvCollector();
+ Utils::LoadConfig();
+ $oOrgCollector->Init();
+
+ $this->assertTrue($oOrgCollector->Collect());
+
+ $sExpected_content = file_get_contents(dirname(__FILE__)."/single_csv/nominal/expected_generated.csv");
+
+ $this->assertEquals($sExpected_content, file_get_contents(APPROOT."/data/iTopPersonCsvCollector-1.csv"));
+ }
+
+ /**
+ * @param $error_file
+ * @param $error_msg
+ * @param bool $exception_msg
+ *
+ * @throws \Exception
+ * @dataProvider ErrorFileProvider
+ */
+ public function testCsvErrors($sErrorFile, $sErrorMsg, $sExceptionMsg = false)
+ {
+ $this->copy(APPROOT."/test/single_csv/common/*");
+ copy(APPROOT."/test/single_csv/csv_errors/$sErrorFile", self::$sCollectorPath."iTopPersonCsvCollector.csv");
+
+ require_once self::$sCollectorPath."iTopPersonCsvCollector.class.inc.php";
+ $orgCollector = new iTopPersonCsvCollector();
+ Utils::LoadConfig();
+ $orgCollector->Init();
+
+ if ($sExceptionMsg) {
+ $this->oMockedLogger->expects($this->exactly(2))
+ ->method("Log")
+ ->withConsecutive([LOG_ERR, $sErrorMsg], [LOG_ERR, $sExceptionMsg]);
+ } else {
+ if ($sErrorMsg) {
+ $this->oMockedLogger->expects($this->exactly(1))
+ ->method("Log");
+ } else {
+ $this->oMockedLogger->expects($this->exactly(0))
+ ->method("Log");
+ }
+ }
+ try {
+ $bResult = $orgCollector->Collect();
+
+ $this->assertEquals($sExceptionMsg ? false : true, $bResult);
+ } catch (Exception $e) {
+ $this->assertEquals($sExceptionMsg, $e->getMessage());
+ }
+ }
+
+ public function ErrorFileProvider()
+ {
+ return [
+ "wrong number of line" => [
+ "wrongnumber_columns_inaline.csv",
+ "[iTopPersonCsvCollector] Wrong number of columns (1) on line 2 (expected 18 columns just like in header): aa",
+ 'iTopPersonCsvCollector::Collect() got an exception: Invalid CSV file.',
+ ],
+ "no primary key" => [
+ "no_primarykey.csv",
+ "[iTopPersonCsvCollector] The mandatory column \"primary_key\" is missing in the csv file.",
+ 'iTopPersonCsvCollector::Collect() got an exception: Missing columns in the csv file.',
+ ],
+ "no email" => [
"no_email.csv",
"[iTopPersonCsvCollector] The field \"email\", used for reconciliation, has missing column(s) in the csv file.",
"iTopPersonCsvCollector::Collect() got an exception: Missing columns in the csv file.",
- ),
- "empty csv" => array(
- "empty_file.csv",
- "[iTopPersonCsvCollector] CSV file is empty. Data collection stops here.",
- "",
- ),
- "empty csv with header" => array(
- "empty_file_with_header.csv",
- "[iTopPersonCsvCollector] CSV file is empty. Data collection stops here.",
- "",
- ),
- "OK" => array("../nominal/iTopPersonCsvCollector.csv", ""),
- );
- }
-
- public function testExploode()
- {
- $stest = "primary_key;first_name;name;org_id;phone;mobile_phone;employee_number;email;function;status";
- $aValues = array(
- "primary_key",
- "first_name",
- "name",
- "org_id",
- "phone",
- "mobile_phone",
- "employee_number",
- "email",
- "function",
- "status",
- );
- $this->assertEquals($aValues, explode(";", $stest));
- }
+ ],
+ "empty csv" => [
+ "empty_file.csv",
+ "[iTopPersonCsvCollector] CSV file is empty. Data collection stops here.",
+ "",
+ ],
+ "empty csv with header" => [
+ "empty_file_with_header.csv",
+ "[iTopPersonCsvCollector] CSV file is empty. Data collection stops here.",
+ "",
+ ],
+ "OK" => ["../nominal/iTopPersonCsvCollector.csv", ""],
+ ];
+ }
+
+ public function testExploode()
+ {
+ $stest = "primary_key;first_name;name;org_id;phone;mobile_phone;employee_number;email;function;status";
+ $aValues = [
+ "primary_key",
+ "first_name",
+ "name",
+ "org_id",
+ "phone",
+ "mobile_phone",
+ "employee_number",
+ "email",
+ "function",
+ "status",
+ ];
+ $this->assertEquals($aValues, explode(";", $stest));
+ }
}
diff --git a/test/FakeCollector.php b/test/FakeCollector.php
index 9c681cf..0fd76e3 100644
--- a/test/FakeCollector.php
+++ b/test/FakeCollector.php
@@ -1,9 +1,11 @@
sSourceName = "fakesource";
- $this->iSourceId = 666;
- }
+class FakeCollector extends Collector
+{
+ public function __construct()
+ {
+ parent::__construct();
+ $this->sSourceName = "fakesource";
+ $this->iSourceId = 666;
+ }
}
diff --git a/test/JsonCollectorTest.php b/test/JsonCollectorTest.php
index 2d9f7db..35d8a92 100644
--- a/test/JsonCollectorTest.php
+++ b/test/JsonCollectorTest.php
@@ -17,197 +17,198 @@
class JsonCollectorTest extends TestCase
{
- private static $sCollectorPath = APPROOT."/collectors/";
- private $oMockedLogger;
+ private static $sCollectorPath = APPROOT."/collectors/";
+ private $oMockedLogger;
- public function setUp(): void
- {
- parent::setUp();
+ public function setUp(): void
+ {
+ parent::setUp();
- $aCollectorFiles = glob(self::$sCollectorPath."*");
- foreach ($aCollectorFiles as $fFile) {
- unlink($fFile);
- }
+ $aCollectorFiles = glob(self::$sCollectorPath."*");
+ foreach ($aCollectorFiles as $fFile) {
+ unlink($fFile);
+ }
- $this->oMockedLogger = $this->createMock("UtilsLogger");
- \Utils::MockLog($this->oMockedLogger);
+ $this->oMockedLogger = $this->createMock("UtilsLogger");
+ \Utils::MockLog($this->oMockedLogger);
- }
+ }
- public function tearDown(): void
- {
- parent::tearDown();
- $aCollectorFiles = glob(self::$sCollectorPath."*");
- foreach ($aCollectorFiles as $file) {
- unlink($file);
- }
- }
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ $aCollectorFiles = glob(self::$sCollectorPath."*");
+ foreach ($aCollectorFiles as $file) {
+ unlink($file);
+ }
+ }
- public function testIOException()
- {
- $this->assertFalse(is_a(new Exception(""), "IOException"));
- $this->assertTrue(is_a(new \IOException(""), "IOException"));
- }
+ public function testIOException()
+ {
+ $this->assertFalse(is_a(new Exception(""), "IOException"));
+ $this->assertTrue(is_a(new \IOException(""), "IOException"));
+ }
- private function copy($sPattern)
- {
- if (!is_dir(self::$sCollectorPath)) {
- mkdir(self::$sCollectorPath);
- }
-
- $aFiles = glob($sPattern);
- foreach ($aFiles as $fFile) {
- if (is_file($fFile)) {
- $sPath = self::$sCollectorPath.basename($fFile);
- $bRes = copy($fFile, $sPath);
- if (!$bRes) {
- throw new \Exception("Failed copying $fFile to $sPath");
- }
- }
- }
- }
+ private function copy($sPattern)
+ {
+ if (!is_dir(self::$sCollectorPath)) {
+ mkdir(self::$sCollectorPath);
+ }
+
+ $aFiles = glob($sPattern);
+ foreach ($aFiles as $fFile) {
+ if (is_file($fFile)) {
+ $sPath = self::$sCollectorPath.basename($fFile);
+ $bRes = copy($fFile, $sPath);
+ if (!$bRes) {
+ throw new \Exception("Failed copying $fFile to $sPath");
+ }
+ }
+ }
+ }
- /*
- * @param string $initDir initial directory of file params.distrib.xml
- */
- private function replaceTranslateRelativePathInParam($initDir)
- {
- $sContent = str_replace("APPROOT", APPROOT, file_get_contents(APPROOT.$initDir."/params.distrib.xml"));
- print_r($sContent);
- $rHandle = fopen(APPROOT."/collectors/params.distrib.xml", "w");
- fwrite($rHandle, $sContent);
- fclose($rHandle);
+ /*
+ * @param string $initDir initial directory of file params.distrib.xml
+ */
+ private function replaceTranslateRelativePathInParam($initDir)
+ {
+ $sContent = str_replace("APPROOT", APPROOT, file_get_contents(APPROOT.$initDir."/params.distrib.xml"));
+ print_r($sContent);
+ $rHandle = fopen(APPROOT."/collectors/params.distrib.xml", "w");
+ fwrite($rHandle, $sContent);
+ fclose($rHandle);
- }
+ }
- /**
- * @param bool $bAdditionalDir
- *
- * @dataProvider OrgCollectorProvider
- * @throws \Exception
- */
- public function testOrgCollector($sAdditionalDir = '')
- {
- $this->copy(APPROOT."/test/single_json/common/*");
- $this->copy(APPROOT."/test/single_json/".$sAdditionalDir."/*");
- $this->replaceTranslateRelativePathInParam("/test/single_json/".$sAdditionalDir);
+ /**
+ * @param bool $bAdditionalDir
+ *
+ * @dataProvider OrgCollectorProvider
+ * @throws \Exception
+ */
+ public function testOrgCollector($sAdditionalDir = '')
+ {
+ $this->copy(APPROOT."/test/single_json/common/*");
+ $this->copy(APPROOT."/test/single_json/".$sAdditionalDir."/*");
+ $this->replaceTranslateRelativePathInParam("/test/single_json/".$sAdditionalDir);
- require_once self::$sCollectorPath."ITopPersonJsonCollector.class.inc.php";
+ require_once self::$sCollectorPath."ITopPersonJsonCollector.class.inc.php";
- $this->oMockedLogger->expects($this->exactly(0))
- ->method("Log");
+ $this->oMockedLogger->expects($this->exactly(0))
+ ->method("Log");
- // WARNING: must call LoadConfig before Init.
- \Utils::LoadConfig();
- $oOrgCollector = new \ITopPersonJsonCollector();
- $oOrgCollector->Init();
+ // WARNING: must call LoadConfig before Init.
+ \Utils::LoadConfig();
+ $oOrgCollector = new \ITopPersonJsonCollector();
+ $oOrgCollector->Init();
- $this->assertTrue($oOrgCollector->Collect());
+ $this->assertTrue($oOrgCollector->Collect());
- $sExpected_content = file_get_contents(self::$sCollectorPath."expected_generated.csv");
+ $sExpected_content = file_get_contents(self::$sCollectorPath."expected_generated.csv");
- $this->assertEquals($sExpected_content, file_get_contents(APPROOT."/data/ITopPersonJsonCollector-1.csv"));
- }
+ $this->assertEquals($sExpected_content, file_get_contents(APPROOT."/data/ITopPersonJsonCollector-1.csv"));
+ }
- public static function OrgCollectorProvider()
- {
- return [
- "multicolumns_attachment" => [ "multicolumns_attachment" ],
- "default_value" => [ "default_value" ],
- "format_json_1" => [ "format_json_1" ],
- "format_json_2" => [ "format_json_2" ],
- "format_json_3" => [ "format_json_3" ],
- "sort of object xpath parsing via a key" => [ "format_json_4" ],
- "sort of object xpath parsing via an index" => [ "format_json_5" ],
- "first row nullified function" => [ "nullified_json_1" ],
- "another row nullified function" => [ "nullified_json_2" ],
+ public static function OrgCollectorProvider()
+ {
+ return [
+ "multicolumns_attachment" => [ "multicolumns_attachment" ],
+ "default_value" => [ "default_value" ],
+ "format_json_1" => [ "format_json_1" ],
+ "format_json_2" => [ "format_json_2" ],
+ "format_json_3" => [ "format_json_3" ],
+ "sort of object xpath parsing via a key" => [ "format_json_4" ],
+ "sort of object xpath parsing via an index" => [ "format_json_5" ],
+ "first row nullified function" => [ "nullified_json_1" ],
+ "another row nullified function" => [ "nullified_json_2" ],
"json file with relative path" => [ "json_file_with_relative_path" ],
- ];
- }
+ ];
+ }
- /**
- * @param $additional_dir
- * @param $error_msg
- * @param bool $exception_msg
- *
- * @throws \Exception
- * @dataProvider ErrorFileProvider
- */
- public function testJsonErrors($sAdditionalDir, $sErrorMsg, $sExceptionMsg = false, $sExceptionMsg3 = false)
- {
- $this->copy(APPROOT."/test/single_json/common/*");
- $this->copy(APPROOT."/test/single_json/json_error/".$sAdditionalDir."/*");
-
- $this->replaceTranslateRelativePathInParam("/test/single_json/json_error/".$sAdditionalDir);
-
- require_once self::$sCollectorPath."ITopPersonJsonCollector.class.inc.php";
- $oOrgCollector = new \ITopPersonJsonCollector();
- \Utils::LoadConfig();
- $oOrgCollector->Init();
-
- if ($sExceptionMsg3) {
- $this->oMockedLogger->expects($this->exactly(3))
- ->method("Log")
- ->withConsecutive([LOG_ERR, $sErrorMsg], [LOG_ERR, $sExceptionMsg], [LOG_ERR, $sExceptionMsg3]);
- } elseif ($sExceptionMsg) {
- $this->oMockedLogger->expects($this->exactly(2))
- ->method("Log")
- ->withConsecutive([LOG_ERR, $sErrorMsg], [LOG_ERR, $sExceptionMsg]);
- } elseif ($sErrorMsg) {
- $this->oMockedLogger->expects($this->exactly(1))
- ->method("Log")
- ->withConsecutive([LOG_ERR, $sErrorMsg]);
- } else {
- $this->oMockedLogger->expects($this->exactly(0))
- ->method("Log");
- }
- try {
- $bResult = $oOrgCollector->Collect();
-
- $this->assertEquals($sErrorMsg ? false : true, $bResult);
- } catch (Exception $e) {
- $this->assertEquals($sExceptionMsg, $e->getMessage());
- }
- }
+ /**
+ * @param $additional_dir
+ * @param $error_msg
+ * @param bool $exception_msg
+ *
+ * @throws \Exception
+ * @dataProvider ErrorFileProvider
+ */
+ public function testJsonErrors($sAdditionalDir, $sErrorMsg, $sExceptionMsg = false, $sExceptionMsg3 = false)
+ {
+ $this->copy(APPROOT."/test/single_json/common/*");
+ $this->copy(APPROOT."/test/single_json/json_error/".$sAdditionalDir."/*");
+
+ $this->replaceTranslateRelativePathInParam("/test/single_json/json_error/".$sAdditionalDir);
+
+ require_once self::$sCollectorPath."ITopPersonJsonCollector.class.inc.php";
+ $oOrgCollector = new \ITopPersonJsonCollector();
+ \Utils::LoadConfig();
+ $oOrgCollector->Init();
+
+ if ($sExceptionMsg3) {
+ $this->oMockedLogger->expects($this->exactly(3))
+ ->method("Log")
+ ->withConsecutive([LOG_ERR, $sErrorMsg], [LOG_ERR, $sExceptionMsg], [LOG_ERR, $sExceptionMsg3]);
+ } elseif ($sExceptionMsg) {
+ $this->oMockedLogger->expects($this->exactly(2))
+ ->method("Log")
+ ->withConsecutive([LOG_ERR, $sErrorMsg], [LOG_ERR, $sExceptionMsg]);
+ } elseif ($sErrorMsg) {
+ $this->oMockedLogger->expects($this->exactly(1))
+ ->method("Log")
+ ->withConsecutive([LOG_ERR, $sErrorMsg]);
+ } else {
+ $this->oMockedLogger->expects($this->exactly(0))
+ ->method("Log");
+ }
+ try {
+ $bResult = $oOrgCollector->Collect();
+
+ $this->assertEquals($sErrorMsg ? false : true, $bResult);
+ } catch (Exception $e) {
+ $this->assertEquals($sExceptionMsg, $e->getMessage());
+ }
+ }
- public static function ErrorFileProvider()
- {
- return [
- "error_json_1" => [
- "error_json_1",
- "[ITopPersonJsonCollector] The field \"first_name\", used for reconciliation, has missing column(s) in the json file.",
- "ITopPersonJsonCollector::Collect() got an exception: Missing columns in the json file.",
- ],
- "error_json_2" => [
- "error_json_2",
- "[ITopPersonJsonCollector] Failed to find path objects/*/blop until data in json file: ".APPROOT."/collectors/dataTest.json.",
- "ITopPersonJsonCollector::Prepare() returned false",
- ],
- "error_json_3" => [
- "error_json_3",
- '[ITopPersonJsonCollector] Failed to translate data from JSON file: \''.APPROOT.'/collectors/dataTest.json\'. Reason: Syntax error',
- "ITopPersonJsonCollector::Prepare() returned false",
- ],
- ];
- }
+ public static function ErrorFileProvider()
+ {
+ return [
+ "error_json_1" => [
+ "error_json_1",
+ "[ITopPersonJsonCollector] The field \"first_name\", used for reconciliation, has missing column(s) in the json file.",
+ "ITopPersonJsonCollector::Collect() got an exception: Missing columns in the json file.",
+ ],
+ "error_json_2" => [
+ "error_json_2",
+ "[ITopPersonJsonCollector] Failed to find path objects/*/blop until data in json file: ".APPROOT."/collectors/dataTest.json.",
+ "ITopPersonJsonCollector::Prepare() returned false",
+ ],
+ "error_json_3" => [
+ "error_json_3",
+ '[ITopPersonJsonCollector] Failed to translate data from JSON file: \''.APPROOT.'/collectors/dataTest.json\'. Reason: Syntax error',
+ "ITopPersonJsonCollector::Prepare() returned false",
+ ],
+ ];
+ }
- public function testFetchWithEmptyJson()
- {
- $this->copy(APPROOT."/test/single_json/common/*");
- require_once self::$sCollectorPath."ITopPersonJsonCollector.class.inc.php";
-
- $oiTopCollector = new \ITopPersonJsonCollector();
- $oiTopCollector->Init();
- try {
- $bResult = $oiTopCollector->Fetch();
- $this->assertEquals(false, $bResult, "JsonCollector::Fetch returns true though CollectoClass::aJson is empty");
- } catch (Exception $e) {
- $this->fail($e->getMessage());
- }
- }
+ public function testFetchWithEmptyJson()
+ {
+ $this->copy(APPROOT."/test/single_json/common/*");
+ require_once self::$sCollectorPath."ITopPersonJsonCollector.class.inc.php";
+
+ $oiTopCollector = new \ITopPersonJsonCollector();
+ $oiTopCollector->Init();
+ try {
+ $bResult = $oiTopCollector->Fetch();
+ $this->assertEquals(false, $bResult, "JsonCollector::Fetch returns true though CollectoClass::aJson is empty");
+ } catch (Exception $e) {
+ $this->fail($e->getMessage());
+ }
+ }
- public function testSearchByKey(){
- $sJson = << "Id",
- 'name' => "Shadok/name"
- ];
-
- $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
- $this->assertEquals(['primary_key' => '1', 'name' => 'gabuzomeu'],
- $aFetchedFields,
- var_export($aFetchedFields, true)
- );
- }
+ $aFieldPaths = [
+ 'primary_key' => "Id",
+ 'name' => "Shadok/name"
+ ];
+
+ $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
+ $this->assertEquals(
+ ['primary_key' => '1', 'name' => 'gabuzomeu'],
+ $aFetchedFields,
+ var_export($aFetchedFields, true)
+ );
+ }
- public function testSearchByKeyAndStar(){
- $sJson = << "*/Id",
- 'name' => "*/Shadok/name"
- ];
-
- $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
- $this->assertEquals(['primary_key' => '1', 'name' => 'gabuzomeu'],
- $aFetchedFields,
- var_export($aFetchedFields, true)
- );
- }
+ $aFieldPaths = [
+ 'primary_key' => "*/Id",
+ 'name' => "*/Shadok/name"
+ ];
+
+ $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
+ $this->assertEquals(
+ ['primary_key' => '1', 'name' => 'gabuzomeu'],
+ $aFetchedFields,
+ var_export($aFetchedFields, true)
+ );
+ }
- public function testSearchInItopJsonStructure(){
- $sJson = << "*/Id",
- 'name' => "*/Shadok/name"
- ];
-
- $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
- $this->assertEquals(['primary_key' => '1', 'name' => 'gabuzomeu'],
- $aFetchedFields,
- var_export($aFetchedFields, true)
- );
- }
+ $aFieldPaths = [
+ 'primary_key' => "*/Id",
+ 'name' => "*/Shadok/name"
+ ];
+
+ $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
+ $this->assertEquals(
+ ['primary_key' => '1', 'name' => 'gabuzomeu'],
+ $aFetchedFields,
+ var_export($aFetchedFields, true)
+ );
+ }
- public function testSearchByKeyAndStar2(){
- $sJson = << "*/Id",
- 'name' => "*/Shadok/name"
- ];
-
- $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
- $this->assertEquals(['primary_key' => '1', 'name' => 'gabuzomeu'],
- $aFetchedFields,
- var_export($aFetchedFields, true)
- );
- }
+ $aFieldPaths = [
+ 'primary_key' => "*/Id",
+ 'name' => "*/Shadok/name"
+ ];
+
+ $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
+ $this->assertEquals(
+ ['primary_key' => '1', 'name' => 'gabuzomeu'],
+ $aFetchedFields,
+ var_export($aFetchedFields, true)
+ );
+ }
- public function testSearchByKeyAndStar3(){
- $sJson = << "*/Id",
- 'name' => "*/Shadok/name"
- ];
-
- $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
- $this->assertEquals(['primary_key' => '1', 'name' => 'gabuzomeu'],
- $aFetchedFields,
- var_export($aFetchedFields, true)
- );
- }
+ $aFieldPaths = [
+ 'primary_key' => "*/Id",
+ 'name' => "*/Shadok/name"
+ ];
+
+ $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
+ $this->assertEquals(
+ ['primary_key' => '1', 'name' => 'gabuzomeu'],
+ $aFetchedFields,
+ var_export($aFetchedFields, true)
+ );
+ }
- public function testSearchByKeyAndStar4(){
- $sJson = << "*/Id",
- 'name' => "*/Shadok/name"
- ];
-
- $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
- $this->assertEquals(['primary_key' => '1', 'name' => 'gabuzomeu'],
- $aFetchedFields,
- var_export($aFetchedFields, true)
- );
- }
+ $aFieldPaths = [
+ 'primary_key' => "*/Id",
+ 'name' => "*/Shadok/name"
+ ];
+
+ $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
+ $this->assertEquals(
+ ['primary_key' => '1', 'name' => 'gabuzomeu'],
+ $aFetchedFields,
+ var_export($aFetchedFields, true)
+ );
+ }
- public function testSearchByKeyAndIndex(){
- $sJson = << "0/Id",
- 'name' => "1/Shadok/name"
- ];
-
- $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
- $this->assertEquals(['primary_key' => '1', 'name' => 'gabuzomeu'],
- $aFetchedFields,
- var_export($aFetchedFields, true)
- );
- }
-
+ $aFieldPaths = [
+ 'primary_key' => "0/Id",
+ 'name' => "1/Shadok/name"
+ ];
+
+ $aFetchedFields = $this->CallSearchFieldValues($sJson, $aFieldPaths);
+ $this->assertEquals(
+ ['primary_key' => '1', 'name' => 'gabuzomeu'],
+ $aFetchedFields,
+ var_export($aFetchedFields, true)
+ );
+ }
- public function CallSearchFieldValues($sJson, $aFieldPaths)
- {
- $this->copy(APPROOT."/test/single_json/common/*");
- $this->copy(APPROOT."/test/single_json/format_json_1/*");
- $this->replaceTranslateRelativePathInParam("/test/single_json/format_json_1");
+ public function CallSearchFieldValues($sJson, $aFieldPaths)
+ {
+ $this->copy(APPROOT."/test/single_json/common/*");
+ $this->copy(APPROOT."/test/single_json/format_json_1/*");
+ $this->replaceTranslateRelativePathInParam("/test/single_json/format_json_1");
- require_once self::$sCollectorPath."ITopPersonJsonCollector.class.inc.php";
+ require_once self::$sCollectorPath."ITopPersonJsonCollector.class.inc.php";
- $this->oMockedLogger->expects($this->exactly(0))
- ->method("Log");
+ $this->oMockedLogger->expects($this->exactly(0))
+ ->method("Log");
- \Utils::LoadConfig();
- $oOrgCollector = new \ITopPersonJsonCollector();
- $oOrgCollector->Init();
+ \Utils::LoadConfig();
+ $oOrgCollector = new \ITopPersonJsonCollector();
+ $oOrgCollector->Init();
- $this->assertTrue($oOrgCollector->Prepare());
+ $this->assertTrue($oOrgCollector->Prepare());
- $aData = json_decode($sJson, true);
+ $aData = json_decode($sJson, true);
- $class = new \ReflectionClass("JsonCollector");
- $method = $class->getMethod("SearchFieldValues");
- $method->setAccessible(true);
- return $method->invokeArgs($oOrgCollector, [$aData, $aFieldPaths]);
- }
+ $class = new \ReflectionClass("JsonCollector");
+ $method = $class->getMethod("SearchFieldValues");
+ $method->setAccessible(true);
+ return $method->invokeArgs($oOrgCollector, [$aData, $aFieldPaths]);
+ }
- public function testFetchWithIgnoredAttributesJson()
- {
- $this->copy(APPROOT."/test/single_json/common/*");
- $this->copy(APPROOT."/test/single_json/ignored_attributes/*");
- require_once self::$sCollectorPath."/ITopPersonJsonCollector.class.inc.php";
- $this->replaceTranslateRelativePathInParam("/test/single_json/ignored_attributes/");
+ public function testFetchWithIgnoredAttributesJson()
+ {
+ $this->copy(APPROOT."/test/single_json/common/*");
+ $this->copy(APPROOT."/test/single_json/ignored_attributes/*");
+ require_once self::$sCollectorPath."/ITopPersonJsonCollector.class.inc.php";
+ $this->replaceTranslateRelativePathInParam("/test/single_json/ignored_attributes/");
- $this->oMockedLogger->expects($this->exactly(0))->method("Log");
+ $this->oMockedLogger->expects($this->exactly(0))->method("Log");
- // WARNING: must call LoadConfig before Init.
- \Utils::LoadConfig();
+ // WARNING: must call LoadConfig before Init.
+ \Utils::LoadConfig();
- $oiTopCollector = new \ITopPersonJsonCollector();
- $oiTopCollector->Init();
- //test private method DataSourcesAreEquivalent
- //this method filled the array aSkippedAttributes used in Collect method
- $oiTopCollector->testDataSourcesAreEquivalent([]);
+ $oiTopCollector = new \ITopPersonJsonCollector();
+ $oiTopCollector->Init();
+ //test private method DataSourcesAreEquivalent
+ //this method filled the array aSkippedAttributes used in Collect method
+ $oiTopCollector->testDataSourcesAreEquivalent([]);
- $this->assertTrue($oiTopCollector->Collect());
+ $this->assertTrue($oiTopCollector->Collect());
- $sExpected_content = file_get_contents(self::$sCollectorPath."expected_generated.csv");
+ $sExpected_content = file_get_contents(self::$sCollectorPath."expected_generated.csv");
- $this->assertEquals($sExpected_content, file_get_contents(APPROOT."/data/ITopPersonJsonCollector-1.csv"));
+ $this->assertEquals($sExpected_content, file_get_contents(APPROOT."/data/ITopPersonJsonCollector-1.csv"));
- }
+ }
}
diff --git a/test/LookupTest.php b/test/LookupTest.php
index b596466..7aaf5d9 100644
--- a/test/LookupTest.php
+++ b/test/LookupTest.php
@@ -21,224 +21,224 @@
class LookupTest extends TestCase
{
- private $oRestClient;
- private $oLookupTable;
- private $oMockedLogger;
+ private $oRestClient;
+ private $oLookupTable;
+ private $oMockedLogger;
- public function setUp(): void
- {
- parent::setUp();
+ public function setUp(): void
+ {
+ parent::setUp();
- $this->oRestClient = $this->createMock(RestClient::class);
+ $this->oRestClient = $this->createMock(RestClient::class);
- LookupTable::SetRestClient($this->oRestClient);
+ LookupTable::SetRestClient($this->oRestClient);
- $this->oMockedLogger = $this->createMock("UtilsLogger");
- Utils::MockLog($this->oMockedLogger, LOG_DEBUG);
+ $this->oMockedLogger = $this->createMock("UtilsLogger");
+ Utils::MockLog($this->oMockedLogger, LOG_DEBUG);
- }
+ }
public function tearDown(): void
- {
- parent::tearDown();
-
- }
-
- /*
- * test function lookup from class LookupTable
- * */
- public function LookupProvider() {
- return [
- 'normal' => [
- 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
- 'aData' => [
- 'OSVersion::61' => [
- 'code' => 0,
- "message" => "",
- "class" => "OSVersion",
- "key" => "61",
- "fields" => [
- "osfamily_id" => "Microsoft Windows 10",
- "osversion_id" => "10.0.19044",
- ],
- ],
- ],
- 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', "10.0.19044"],
- 'aLookupFields' => ["osfamily_id", "osversion_id"],
- 'sDestField' => 'osversion_id',
- 'iFieldIndex' => 3,
- 'bSkipIfEmpty' => false,
- 'bCaseSensitive' => false,
- 'bIgnoreMappingErrors' => false,
- 'sExpectedRes' => true,
- 'sExpectedErrorType' => '',
- 'sExpectedErrorMessage' => '',
- 'sExpectedValue' => 61,
- ],
- 'casesensitive_true' => [
- 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
- 'aData' => [
- 'OSVersion::61' => [
- 'code' => 0,
- "message" => "",
- "class" => "OSVersion",
- "key" => "61",
- "fields" => [
- "osfamily_id" => "Microsoft Windows 10",
- "osversion_id" => "10.0.19044",
- ],
- ],
- ],
- 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', "10.0.19044"],
- 'aLookupFields' => ["osfamily_id", "osversion_id"],
- 'sDestField' => 'osversion_id',
- 'iFieldIndex' => 3,
- 'bSkipIfEmpty' => false,
- 'bCaseSensitive' => true,
- 'bIgnoreMappingErrors' => false,
- 'sExpectedRes' => true,
- 'sExpectedErrorType' => '',
- 'sExpectedErrorMessage' => '',
- 'sExpectedValue' => 61,
- ],
- 'casesensitive_true_error' => [
- 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
- 'aData' => [
- 'OSVersion::61' => [
- 'code' => 0,
- "message" => "",
- "class" => "OSVersion",
- "key" => "61",
- "fields" => [
- "osfamily_id" => "microsoft Windows 10",
- "osversion_id" => "10.0.19044",
- ],
- ],
- ],
- 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', "10.0.19044"],
- 'aLookupFields' => ["osfamily_id", "osversion_id"],
- 'sDestField' => 'osversion_id',
- 'iFieldIndex' => 3,
- 'bSkipIfEmpty' => false,
- 'bCaseSensitive' => true,
- 'bIgnoreMappingErrors' => false,
- 'sExpectedRes' => false,
- 'sExpectedErrorType' => LOG_WARNING,
- 'sExpectedErrorMessage' => 'No mapping found with key: \'Microsoft Windows 10_10.0.19044\', \'osversion_id\' will be set to zero.',
- 'sExpectedValue' => 61,
- ],
- "error_fieldNotFound_dontIgnoreMappingError" => [
- 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
- 'aData' => [
- 'OSVersion::61' => [
- 'code' => 0,
- "message" => "",
- "class" => "OSVersion",
- "key" => "61",
- "fields" => [
- "osfamily_id" => "Microsoft Windows 10",
- "osversion_id" => "10.0.19044",
- ],
- ],
- ],
- 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', "10.0.190445"],
- 'aLookupFields' => ["osfamily_id", "osversion_id"],
- 'sDestField' => 'osversion_id',
- 'iFieldIndex' => 3,
- 'bSkipIfEmpty' => false,
- 'bCaseSensitive' => false,
- 'bIgnoreMappingErrors' => false,
- 'sExpectedRes' => '',
- 'sExpectedErrorType' => LOG_WARNING,
- 'sExpectedErrorMessage' => 'No mapping found with key: \'microsoft windows 10_10.0.190445\', \'osversion_id\' will be set to zero.',
- 'sExpectedValue' => 2,
- ],
- "error_fieldNotFound_ignoreMappingError" => [
- 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
- 'aData' => [
- 'OSVersion::61' => [
- 'code' => 0,
- "message" => "",
- "class" => "OSVersion",
- "key" => "61",
- "fields" => [
- "osfamily_id" => "Microsoft Windows 10",
- "osversion_id" => "10.0.19044",
- ],
- ],
- ],
- 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', "10.0.190445"],
- 'aLookupFields' => ["osfamily_id", "osversion_id"],
- 'sDestField' => 'osversion_id',
- 'iFieldIndex' => 3,
- 'bSkipIfEmpty' => false,
- 'bCaseSensitive' => false,
- 'bIgnoreMappingErrors' => true,
- 'sExpectedRes' => true,
- 'sExpectedErrorType' => LOG_DEBUG,
- 'sExpectedErrorMessage' => 'No mapping found with key: \'microsoft windows 10_10.0.190445\', \'osversion_id\' will be set to zero.',
- 'sExpectedValue' => "10.0.190445",
- ],
- "emptyfield" => [
- 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
- 'aData' => [
- 'OSVersion::61' => [
- 'code' => 0,
- "message" => "",
- "class" => "OSVersion",
- "key" => "61",
- "fields" => [
- "osfamily_id" => "Microsoft Windows 10",
- "osversion_id" => "10.0.19044",
- ],
- ],
- ],
- 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', ''],
- 'aLookupFields' => ["osfamily_id", "osversion_id"],
- 'sDestField' => 'osversion_id',
- 'iFieldIndex' => 3,
- 'bSkipIfEmpty' => true,
- 'bCaseSensitive' => false,
- 'bIgnoreMappingErrors' => false,
- 'sExpectedRes' => '',
- 'sExpectedErrorType' => '',
- 'sExpectedErrorMessage' => '',
- 'sExpectedValue' => 2,
- ],
- ];
- }
-
- /**
- * @dataProvider LookupProvider
- */
- public function testLookup($aFirstLine, $aData, $aLineData, $aLookupFields, $sDestField, $iFieldIndex, $bSkipIfEmpty, $bCaseSensitive, $bIgnoreMappingErrors, $sExpectedRes, $sExpectedErrorType, $sExpectedErrorMessage, $sExpectedValue)
- {
- $this->oRestClient->expects($this->once())
- ->method('Get')
- ->with('OSVersion', 'SELECT OSVersion', 'osfamily_id,osversion_id')
- ->willReturn([
- 'code' => 0,
- 'objects' => $aData
- ]) ;
- $this->oLookupTable = new LookupTable('SELECT OSVersion', $aLookupFields,$bCaseSensitive, $bIgnoreMappingErrors );
-
- if($sExpectedErrorType != ''){
- $this->oMockedLogger->expects($this->once())
- ->method('Log')
- ->with($sExpectedErrorType, $sExpectedErrorMessage) ;
- } else {
- $this->oMockedLogger->expects($this->never())
- ->method('Log') ;
-
- }
- $this->oLookupTable->Lookup($aFirstLine, $aLookupFields, $sDestField, 0);
- $sRes = $this->oLookupTable->Lookup($aLineData, $aLookupFields, $sDestField, 1, $bSkipIfEmpty);
-
-
- $this->assertEquals( $sExpectedRes,$sRes );
-
- if ($sRes) {
- $this->assertEquals($sExpectedValue,$aLineData[$iFieldIndex]);
- }
-}
+ {
+ parent::tearDown();
+
+ }
+
+ /*
+ * test function lookup from class LookupTable
+ * */
+ public function LookupProvider()
+ {
+ return [
+ 'normal' => [
+ 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
+ 'aData' => [
+ 'OSVersion::61' => [
+ 'code' => 0,
+ "message" => "",
+ "class" => "OSVersion",
+ "key" => "61",
+ "fields" => [
+ "osfamily_id" => "Microsoft Windows 10",
+ "osversion_id" => "10.0.19044",
+ ],
+ ],
+ ],
+ 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', "10.0.19044"],
+ 'aLookupFields' => ["osfamily_id", "osversion_id"],
+ 'sDestField' => 'osversion_id',
+ 'iFieldIndex' => 3,
+ 'bSkipIfEmpty' => false,
+ 'bCaseSensitive' => false,
+ 'bIgnoreMappingErrors' => false,
+ 'sExpectedRes' => true,
+ 'sExpectedErrorType' => '',
+ 'sExpectedErrorMessage' => '',
+ 'sExpectedValue' => 61,
+ ],
+ 'casesensitive_true' => [
+ 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
+ 'aData' => [
+ 'OSVersion::61' => [
+ 'code' => 0,
+ "message" => "",
+ "class" => "OSVersion",
+ "key" => "61",
+ "fields" => [
+ "osfamily_id" => "Microsoft Windows 10",
+ "osversion_id" => "10.0.19044",
+ ],
+ ],
+ ],
+ 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', "10.0.19044"],
+ 'aLookupFields' => ["osfamily_id", "osversion_id"],
+ 'sDestField' => 'osversion_id',
+ 'iFieldIndex' => 3,
+ 'bSkipIfEmpty' => false,
+ 'bCaseSensitive' => true,
+ 'bIgnoreMappingErrors' => false,
+ 'sExpectedRes' => true,
+ 'sExpectedErrorType' => '',
+ 'sExpectedErrorMessage' => '',
+ 'sExpectedValue' => 61,
+ ],
+ 'casesensitive_true_error' => [
+ 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
+ 'aData' => [
+ 'OSVersion::61' => [
+ 'code' => 0,
+ "message" => "",
+ "class" => "OSVersion",
+ "key" => "61",
+ "fields" => [
+ "osfamily_id" => "microsoft Windows 10",
+ "osversion_id" => "10.0.19044",
+ ],
+ ],
+ ],
+ 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', "10.0.19044"],
+ 'aLookupFields' => ["osfamily_id", "osversion_id"],
+ 'sDestField' => 'osversion_id',
+ 'iFieldIndex' => 3,
+ 'bSkipIfEmpty' => false,
+ 'bCaseSensitive' => true,
+ 'bIgnoreMappingErrors' => false,
+ 'sExpectedRes' => false,
+ 'sExpectedErrorType' => LOG_WARNING,
+ 'sExpectedErrorMessage' => 'No mapping found with key: \'Microsoft Windows 10_10.0.19044\', \'osversion_id\' will be set to zero.',
+ 'sExpectedValue' => 61,
+ ],
+ "error_fieldNotFound_dontIgnoreMappingError" => [
+ 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
+ 'aData' => [
+ 'OSVersion::61' => [
+ 'code' => 0,
+ "message" => "",
+ "class" => "OSVersion",
+ "key" => "61",
+ "fields" => [
+ "osfamily_id" => "Microsoft Windows 10",
+ "osversion_id" => "10.0.19044",
+ ],
+ ],
+ ],
+ 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', "10.0.190445"],
+ 'aLookupFields' => ["osfamily_id", "osversion_id"],
+ 'sDestField' => 'osversion_id',
+ 'iFieldIndex' => 3,
+ 'bSkipIfEmpty' => false,
+ 'bCaseSensitive' => false,
+ 'bIgnoreMappingErrors' => false,
+ 'sExpectedRes' => '',
+ 'sExpectedErrorType' => LOG_WARNING,
+ 'sExpectedErrorMessage' => 'No mapping found with key: \'microsoft windows 10_10.0.190445\', \'osversion_id\' will be set to zero.',
+ 'sExpectedValue' => 2,
+ ],
+ "error_fieldNotFound_ignoreMappingError" => [
+ 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
+ 'aData' => [
+ 'OSVersion::61' => [
+ 'code' => 0,
+ "message" => "",
+ "class" => "OSVersion",
+ "key" => "61",
+ "fields" => [
+ "osfamily_id" => "Microsoft Windows 10",
+ "osversion_id" => "10.0.19044",
+ ],
+ ],
+ ],
+ 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', "10.0.190445"],
+ 'aLookupFields' => ["osfamily_id", "osversion_id"],
+ 'sDestField' => 'osversion_id',
+ 'iFieldIndex' => 3,
+ 'bSkipIfEmpty' => false,
+ 'bCaseSensitive' => false,
+ 'bIgnoreMappingErrors' => true,
+ 'sExpectedRes' => true,
+ 'sExpectedErrorType' => LOG_DEBUG,
+ 'sExpectedErrorMessage' => 'No mapping found with key: \'microsoft windows 10_10.0.190445\', \'osversion_id\' will be set to zero.',
+ 'sExpectedValue' => "10.0.190445",
+ ],
+ "emptyfield" => [
+ 'aFirstLine' => ["primary_key", "name", "osfamily_id", "osversion_id"],
+ 'aData' => [
+ 'OSVersion::61' => [
+ 'code' => 0,
+ "message" => "",
+ "class" => "OSVersion",
+ "key" => "61",
+ "fields" => [
+ "osfamily_id" => "Microsoft Windows 10",
+ "osversion_id" => "10.0.19044",
+ ],
+ ],
+ ],
+ 'aLineData' => ["1", "test_normal", 'Microsoft Windows 10', ''],
+ 'aLookupFields' => ["osfamily_id", "osversion_id"],
+ 'sDestField' => 'osversion_id',
+ 'iFieldIndex' => 3,
+ 'bSkipIfEmpty' => true,
+ 'bCaseSensitive' => false,
+ 'bIgnoreMappingErrors' => false,
+ 'sExpectedRes' => '',
+ 'sExpectedErrorType' => '',
+ 'sExpectedErrorMessage' => '',
+ 'sExpectedValue' => 2,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider LookupProvider
+ */
+ public function testLookup($aFirstLine, $aData, $aLineData, $aLookupFields, $sDestField, $iFieldIndex, $bSkipIfEmpty, $bCaseSensitive, $bIgnoreMappingErrors, $sExpectedRes, $sExpectedErrorType, $sExpectedErrorMessage, $sExpectedValue)
+ {
+ $this->oRestClient->expects($this->once())
+ ->method('Get')
+ ->with('OSVersion', 'SELECT OSVersion', 'osfamily_id,osversion_id')
+ ->willReturn([
+ 'code' => 0,
+ 'objects' => $aData
+ ]) ;
+ $this->oLookupTable = new LookupTable('SELECT OSVersion', $aLookupFields, $bCaseSensitive, $bIgnoreMappingErrors);
+
+ if ($sExpectedErrorType != '') {
+ $this->oMockedLogger->expects($this->once())
+ ->method('Log')
+ ->with($sExpectedErrorType, $sExpectedErrorMessage) ;
+ } else {
+ $this->oMockedLogger->expects($this->never())
+ ->method('Log') ;
+
+ }
+ $this->oLookupTable->Lookup($aFirstLine, $aLookupFields, $sDestField, 0);
+ $sRes = $this->oLookupTable->Lookup($aLineData, $aLookupFields, $sDestField, 1, $bSkipIfEmpty);
+
+ $this->assertEquals($sExpectedRes, $sRes);
+
+ if ($sRes) {
+ $this->assertEquals($sExpectedValue, $aLineData[$iFieldIndex]);
+ }
+ }
}
diff --git a/test/OrchestratorTest.php b/test/OrchestratorTest.php
index 4175161..1ff0d0c 100644
--- a/test/OrchestratorTest.php
+++ b/test/OrchestratorTest.php
@@ -18,75 +18,74 @@
class OrchestratorTest extends TestCase
{
- private static $sCollectorPath = APPROOT."/collectors/";
- private $oMockedLogger;
-
- public function setUp(): void
- {
- parent::setUp();
-
- $aCollectorFiles = glob(self::$sCollectorPath."*");
- foreach ($aCollectorFiles as $sFile) {
- unlink($sFile);
- }
-
- $this->oMockedLogger = $this->createMock("UtilsLogger");
- Utils::MockLog($this->oMockedLogger);
- }
-
- public function tearDown(): void
- {
- parent::tearDown();
- $aCollectorFiles = glob(self::$sCollectorPath."*");
- foreach ($aCollectorFiles as $sFile) {
- unlink($sFile);
- }
- }
-
- /**
- * @param bool $sAdditionalDir
- *
- * @dataProvider OrgCollectorProvider
- * @throws \Exception
- */
- public function testOrgCollectorGetProjectName($sExpectedProjectName, $sAdditionalDir = false)
- {
- $this->copy(APPROOT."/test/getproject/common/*");
- $this->copy(APPROOT."/test/getproject/$sAdditionalDir/*");
-
- require_once self::$sCollectorPath."iTopPersonCsvCollector.class.inc.php";
-
-
- $this->oMockedLogger->expects($this->exactly(0))
- ->method("Log");
-
- $oOrgCollector = new iTopPersonCsvCollector();
- $oOrgCollector->Init();
- $this->assertEquals($sExpectedProjectName, $oOrgCollector->GetProjectName());
- }
-
- private function copy($sPattern)
- {
- if (!is_dir(self::$sCollectorPath)) {
- mkdir(self::$sCollectorPath);
- }
-
- $aFiles = glob($sPattern);
- foreach ($aFiles as $sFile) {
- if (is_file($sFile)) {
- $bRes = copy($sFile, self::$sCollectorPath.basename($sFile));
- if (!$bRes) {
- throw new Exception("Failed copying $sFile to ".self::$sCollectorPath.basename($sFile));
- }
- }
- }
- }
-
- public function OrgCollectorProvider()
- {
- return array(
- "empty_module_file" => array("myproject"),
- "module" => array("centreon-collector", "module"),
- );
- }
+ private static $sCollectorPath = APPROOT."/collectors/";
+ private $oMockedLogger;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $aCollectorFiles = glob(self::$sCollectorPath."*");
+ foreach ($aCollectorFiles as $sFile) {
+ unlink($sFile);
+ }
+
+ $this->oMockedLogger = $this->createMock("UtilsLogger");
+ Utils::MockLog($this->oMockedLogger);
+ }
+
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ $aCollectorFiles = glob(self::$sCollectorPath."*");
+ foreach ($aCollectorFiles as $sFile) {
+ unlink($sFile);
+ }
+ }
+
+ /**
+ * @param bool $sAdditionalDir
+ *
+ * @dataProvider OrgCollectorProvider
+ * @throws \Exception
+ */
+ public function testOrgCollectorGetProjectName($sExpectedProjectName, $sAdditionalDir = false)
+ {
+ $this->copy(APPROOT."/test/getproject/common/*");
+ $this->copy(APPROOT."/test/getproject/$sAdditionalDir/*");
+
+ require_once self::$sCollectorPath."iTopPersonCsvCollector.class.inc.php";
+
+ $this->oMockedLogger->expects($this->exactly(0))
+ ->method("Log");
+
+ $oOrgCollector = new iTopPersonCsvCollector();
+ $oOrgCollector->Init();
+ $this->assertEquals($sExpectedProjectName, $oOrgCollector->GetProjectName());
+ }
+
+ private function copy($sPattern)
+ {
+ if (!is_dir(self::$sCollectorPath)) {
+ mkdir(self::$sCollectorPath);
+ }
+
+ $aFiles = glob($sPattern);
+ foreach ($aFiles as $sFile) {
+ if (is_file($sFile)) {
+ $bRes = copy($sFile, self::$sCollectorPath.basename($sFile));
+ if (!$bRes) {
+ throw new Exception("Failed copying $sFile to ".self::$sCollectorPath.basename($sFile));
+ }
+ }
+ }
+ }
+
+ public function OrgCollectorProvider()
+ {
+ return [
+ "empty_module_file" => ["myproject"],
+ "module" => ["centreon-collector", "module"],
+ ];
+ }
}
diff --git a/test/ParametersTest.php b/test/ParametersTest.php
index 18dfccf..4e21a81 100644
--- a/test/ParametersTest.php
+++ b/test/ParametersTest.php
@@ -1,4 +1,5 @@
SetNonPublicProperty($oParameters, 'aData', $aData);
-
- $sDumpParameters = $oParameters->Dump();
-
- $this->assertStringContainsString($sExpectedDump, $sDumpParameters);
- }
-
- public function ToXMLProvider() {
- return [
- 'Parameter with &' => [
- 'aData' => ['escaped_param' => '(&(objectClass=person)(mail=*))'],
- 'sExpectedDump' => '(&(objectClass=person)(mail=*))',
- ],
- 'Parameter with array' => [
- 'aData' => ['paramroot' => ['param1' => 'param1val', 'param2' => 'param2val']],
- 'sExpectedDump' => "\n param1val\n param2val\n ",
- ],
- ];
- }
-
- public function SetNonPublicProperty(object $oObject, string $sProperty, $value)
- {
- $oProperty = $this->GetProperty(get_class($oObject), $sProperty);
- $oProperty->setValue($oObject, $value);
- }
- private function GetProperty(string $sClass, string $sProperty): \ReflectionProperty
- {
- $class = new \ReflectionClass($sClass);
- $property = $class->getProperty($sProperty);
- $property->setAccessible(true);
-
- return $property;
- }
+class ParametersTest extends TestCase
+{
+ /**
+ * @dataProvider ToXMLProvider
+ */
+ public function testToXML(array $aData, string $sExpectedDump)
+ {
+ $oParameters = new Parameters();
+ $this->SetNonPublicProperty($oParameters, 'aData', $aData);
+
+ $sDumpParameters = $oParameters->Dump();
+
+ $this->assertStringContainsString($sExpectedDump, $sDumpParameters);
+ }
+
+ public function ToXMLProvider()
+ {
+ return [
+ 'Parameter with &' => [
+ 'aData' => ['escaped_param' => '(&(objectClass=person)(mail=*))'],
+ 'sExpectedDump' => '(&(objectClass=person)(mail=*))',
+ ],
+ 'Parameter with array' => [
+ 'aData' => ['paramroot' => ['param1' => 'param1val', 'param2' => 'param2val']],
+ 'sExpectedDump' => "\n param1val\n param2val\n ",
+ ],
+ ];
+ }
+
+ public function SetNonPublicProperty(object $oObject, string $sProperty, $value)
+ {
+ $oProperty = $this->GetProperty(get_class($oObject), $sProperty);
+ $oProperty->setValue($oObject, $value);
+ }
+ private function GetProperty(string $sClass, string $sProperty): \ReflectionProperty
+ {
+ $class = new \ReflectionClass($sClass);
+ $property = $class->getProperty($sProperty);
+ $property->setAccessible(true);
+
+ return $property;
+ }
}
diff --git a/test/RestTest.php b/test/RestTest.php
index 0c8cfe5..6a1df65 100644
--- a/test/RestTest.php
+++ b/test/RestTest.php
@@ -16,104 +16,106 @@
class RestTest extends TestCase
{
- public function setUp(): void
- {
- parent::setUp();
- }
+ public function setUp(): void
+ {
+ parent::setUp();
+ }
- public function tearDown(): void
- {
- parent::tearDown();
- Utils::MockDoPostRequestService(null);
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ Utils::MockDoPostRequestService(null);
- $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
- $reflection->setAccessible(true);
- $reflection->setValue(null, null);
- }
+ $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
+ $reflection->setAccessible(true);
+ $reflection->setValue(null, null);
+ }
- public function GetCredentialsProvider(){
- return [
- 'login/password (nominal)' => [
- 'aParameters' => [
- 'itop_url' => 'URI',
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2'
- ],
- 'aExpectedCredentials' => ['auth_user'=> 'admin1', 'auth_pwd'=>'admin2'],
- 'url' => 'URI/webservices/rest.php?login_mode=form&version=1.0'
- ],
- 'new token' => [
- 'aParameters' => [
- 'itop_url' => 'URI',
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_token' => 'admin4',
- ],
- 'aExpectedCredentials' => ['auth_token'=> 'admin4'],
- 'url' => 'URI/webservices/rest.php?login_mode=token&version=1.0'
- ],
- 'new token over legacy one' => [
- 'aParameters' => [
- 'itop_url' => 'URI',
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_rest_token' => 'admin3',
- 'itop_token' => 'admin4',
- ],
- 'aExpectedCredentials' => ['auth_token'=> 'admin4'],
- 'url' => 'URI/webservices/rest.php?login_mode=token&version=1.0'
- ],
- 'configured login_mode' => [
- 'aParameters' => [
- 'itop_url' => 'URI',
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_rest_token' => 'admin3',
- 'itop_token' => 'admin4',
- 'itop_login_mode' => 'newloginform',
- ],
- 'aExpectedCredentials' => ['auth_token'=> 'admin4'],
- 'url' => 'URI/webservices/rest.php?login_mode=newloginform&version=1.0'
- ],
- ];
- }
+ public function GetCredentialsProvider()
+ {
+ return [
+ 'login/password (nominal)' => [
+ 'aParameters' => [
+ 'itop_url' => 'URI',
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2'
+ ],
+ 'aExpectedCredentials' => ['auth_user' => 'admin1', 'auth_pwd' => 'admin2'],
+ 'url' => 'URI/webservices/rest.php?login_mode=form&version=1.0'
+ ],
+ 'new token' => [
+ 'aParameters' => [
+ 'itop_url' => 'URI',
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_token' => 'admin4',
+ ],
+ 'aExpectedCredentials' => ['auth_token' => 'admin4'],
+ 'url' => 'URI/webservices/rest.php?login_mode=token&version=1.0'
+ ],
+ 'new token over legacy one' => [
+ 'aParameters' => [
+ 'itop_url' => 'URI',
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_rest_token' => 'admin3',
+ 'itop_token' => 'admin4',
+ ],
+ 'aExpectedCredentials' => ['auth_token' => 'admin4'],
+ 'url' => 'URI/webservices/rest.php?login_mode=token&version=1.0'
+ ],
+ 'configured login_mode' => [
+ 'aParameters' => [
+ 'itop_url' => 'URI',
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_rest_token' => 'admin3',
+ 'itop_token' => 'admin4',
+ 'itop_login_mode' => 'newloginform',
+ ],
+ 'aExpectedCredentials' => ['auth_token' => 'admin4'],
+ 'url' => 'URI/webservices/rest.php?login_mode=newloginform&version=1.0'
+ ],
+ ];
+ }
- /**
- * @dataProvider GetCredentialsProvider
- */
- public function testCallItopViaHttp($aParameters, $aExpectedCredentials, $sExpectedUrl){
- $oParametersMock = $this->createMock(\Parameters::class);
- $oParametersMock->expects($this->atLeast(1))
- ->method('Get')
- ->will($this->returnCallback(
- function($sKey, $aDefaultValue) use ($aParameters) {
- if (array_key_exists($sKey, $aParameters)){
- return $aParameters[$sKey];
- }
- return $aDefaultValue;
- }
- ));
+ /**
+ * @dataProvider GetCredentialsProvider
+ */
+ public function testCallItopViaHttp($aParameters, $aExpectedCredentials, $sExpectedUrl)
+ {
+ $oParametersMock = $this->createMock(\Parameters::class);
+ $oParametersMock->expects($this->atLeast(1))
+ ->method('Get')
+ ->will($this->returnCallback(
+ function ($sKey, $aDefaultValue) use ($aParameters) {
+ if (array_key_exists($sKey, $aParameters)) {
+ return $aParameters[$sKey];
+ }
+ return $aDefaultValue;
+ }
+ ));
- $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
- $reflection->setAccessible(true);
- $reflection->setValue(null, $oParametersMock);
+ $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
+ $reflection->setAccessible(true);
+ $reflection->setValue(null, $oParametersMock);
- $oMockedDoPostRequestService = $this->createMock(DoPostRequestService::class);
- Utils::MockDoPostRequestService($oMockedDoPostRequestService);
+ $oMockedDoPostRequestService = $this->createMock(DoPostRequestService::class);
+ Utils::MockDoPostRequestService($oMockedDoPostRequestService);
- $aListParams = array(
- 'operation' => 'list_operations', // operation code
- 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
- );
- $aAdditionalData = ['json_data' => json_encode($aListParams)];
- $oMockedDoPostRequestService->expects($this->once())
- ->method('DoPostRequest')
- ->with($sExpectedUrl, array_merge($aExpectedCredentials, $aAdditionalData ))
- ->willReturn(json_encode(['retcode' => 0]));
- ;
+ $aListParams = [
+ 'operation' => 'list_operations', // operation code
+ 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
+ ];
+ $aAdditionalData = ['json_data' => json_encode($aListParams)];
+ $oMockedDoPostRequestService->expects($this->once())
+ ->method('DoPostRequest')
+ ->with($sExpectedUrl, array_merge($aExpectedCredentials, $aAdditionalData))
+ ->willReturn(json_encode(['retcode' => 0]));
+ ;
- $oRestClient = new RestClient();
- $this->assertEquals(['retcode' => 0], $oRestClient->ListOperations());
- }
+ $oRestClient = new RestClient();
+ $this->assertEquals(['retcode' => 0], $oRestClient->ListOperations());
+ }
}
diff --git a/test/UtilsTest.php b/test/UtilsTest.php
index a1a6d1c..fce8dc6 100644
--- a/test/UtilsTest.php
+++ b/test/UtilsTest.php
@@ -13,250 +13,259 @@
class UtilsTest extends TestCase
{
- public function setUp(): void
- {
- parent::setUp();
- }
+ public function setUp(): void
+ {
+ parent::setUp();
+ }
- public function tearDown(): void
- {
- parent::tearDown();
- Utils::MockDoPostRequestService(null);
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ Utils::MockDoPostRequestService(null);
- $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
- $reflection->setAccessible(true);
- $reflection->setValue(null, null);
- }
+ $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
+ $reflection->setAccessible(true);
+ $reflection->setValue(null, null);
+ }
- public function ComputeCurlOptionsProvider(){
- return [
- 'nominal usecase: constant key/ constant int value' => [
- 'aRawCurlOptions' => [
- CURLOPT_SSLVERSION => CURL_SSLVERSION_SSLv3,
- ],
- 'aExpectedReturnedOptions' => [
- 32 => 3,
- 78 => 600,
- 13 => 600,
- ]
- ],
- 'nominal usecase: constant key/ constant provided as a string' => [
- 'aRawCurlOptions' => [
- CURLOPT_SSLVERSION => 'CURL_SSLVERSION_SSLv3',
- ],
- 'aExpectedReturnedOptions' => [
- 32 => 3,
- 78 => 600,
- 13 => 600,
- ]
- ],
- 'constant provided as a string key/ string value' => [
- 'aRawCurlOptions' => [
- 'CURLOPT_COOKIE' => 'itop',
- ],
- 'aExpectedReturnedOptions' => [
- 78 => 600,
- 13 => 600,
- 10022 => 'itop',
- ]
- ],
- 'constant key/ constant boolean value' => [
- 'aRawCurlOptions' => [
- CURLOPT_COOKIESESSION => true,
- ],
- 'aExpectedReturnedOptions' => [
- 78 => 600,
- 13 => 600,
- 96 => true,
- ]
- ],
- ];
- }
+ public function ComputeCurlOptionsProvider()
+ {
+ return [
+ 'nominal usecase: constant key/ constant int value' => [
+ 'aRawCurlOptions' => [
+ CURLOPT_SSLVERSION => CURL_SSLVERSION_SSLv3,
+ ],
+ 'aExpectedReturnedOptions' => [
+ 32 => 3,
+ 78 => 600,
+ 13 => 600,
+ ]
+ ],
+ 'nominal usecase: constant key/ constant provided as a string' => [
+ 'aRawCurlOptions' => [
+ CURLOPT_SSLVERSION => 'CURL_SSLVERSION_SSLv3',
+ ],
+ 'aExpectedReturnedOptions' => [
+ 32 => 3,
+ 78 => 600,
+ 13 => 600,
+ ]
+ ],
+ 'constant provided as a string key/ string value' => [
+ 'aRawCurlOptions' => [
+ 'CURLOPT_COOKIE' => 'itop',
+ ],
+ 'aExpectedReturnedOptions' => [
+ 78 => 600,
+ 13 => 600,
+ 10022 => 'itop',
+ ]
+ ],
+ 'constant key/ constant boolean value' => [
+ 'aRawCurlOptions' => [
+ CURLOPT_COOKIESESSION => true,
+ ],
+ 'aExpectedReturnedOptions' => [
+ 78 => 600,
+ 13 => 600,
+ 96 => true,
+ ]
+ ],
+ ];
+ }
- /**
- * @dataProvider ComputeCurlOptionsProvider
- */
- public function testComputeCurlOptions($aRawCurlOptions, $aExpectedReturnedOptions){
- $aCurlOptions = Utils::ComputeCurlOptions($aRawCurlOptions, 600);
+ /**
+ * @dataProvider ComputeCurlOptionsProvider
+ */
+ public function testComputeCurlOptions($aRawCurlOptions, $aExpectedReturnedOptions)
+ {
+ $aCurlOptions = Utils::ComputeCurlOptions($aRawCurlOptions, 600);
- $this->assertEquals($aExpectedReturnedOptions, $aCurlOptions);
- }
+ $this->assertEquals($aExpectedReturnedOptions, $aCurlOptions);
+ }
+ public function ComputeCurlOptionsTimeoutProvider()
+ {
+ return [
+ 'with timeout' => [
+ 'aExpectedReturnedOptions' => [
+ 78 => 600,
+ 13 => 600,
+ ],
+ 'iTimeout' => 600
+ ],
+ 'without timeout' => [
+ 'aExpectedReturnedOptions' => [],
+ 'iTimeout' => -1
+ ],
+ ];
+ }
- public function ComputeCurlOptionsTimeoutProvider(){
- return [
- 'with timeout' => [
- 'aExpectedReturnedOptions' => [
- 78 => 600,
- 13 => 600,
- ],
- 'iTimeout' => 600
- ],
- 'without timeout' => [
- 'aExpectedReturnedOptions' => [],
- 'iTimeout' => -1
- ],
- ];
- }
+ /**
+ * @dataProvider ComputeCurlOptionsTimeoutProvider
+ */
+ public function testComputeCurlOptionsTimeoutProvider($aExpectedReturnedOptions, $iTimeout)
+ {
+ $aRawCurlOptions = [];
- /**
- * @dataProvider ComputeCurlOptionsTimeoutProvider
- */
- public function testComputeCurlOptionsTimeoutProvider($aExpectedReturnedOptions, $iTimeout){
- $aRawCurlOptions = [];
+ $aCurlOptions = Utils::ComputeCurlOptions($aRawCurlOptions, $iTimeout);
- $aCurlOptions = Utils::ComputeCurlOptions($aRawCurlOptions, $iTimeout);
+ $this->assertEquals($aExpectedReturnedOptions, $aCurlOptions);
+ }
- $this->assertEquals($aExpectedReturnedOptions, $aCurlOptions);
- }
+ public function GetCredentialsProvider()
+ {
+ return [
+ 'login/password (nominal)' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2'
+ ],
+ 'aExpectedCredentials' => ['auth_user' => 'admin1', 'auth_pwd' => 'admin2']
+ ],
+ 'new token' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_token' => 'admin4',
+ ],
+ 'aExpectedCredentials' => ['auth_token' => 'admin4']
+ ],
+ 'new token over legacy one' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_rest_token' => 'admin3',
+ 'itop_token' => 'admin4',
+ ],
+ 'aExpectedCredentials' => ['auth_token' => 'admin4']
+ ],
+ ];
+ }
- public function GetCredentialsProvider(){
- return [
- 'login/password (nominal)' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2'
- ],
- 'aExpectedCredentials' => ['auth_user'=> 'admin1', 'auth_pwd'=>'admin2']
- ],
- 'new token' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_token' => 'admin4',
- ],
- 'aExpectedCredentials' => ['auth_token'=> 'admin4']
- ],
- 'new token over legacy one' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_rest_token' => 'admin3',
- 'itop_token' => 'admin4',
- ],
- 'aExpectedCredentials' => ['auth_token'=> 'admin4']
- ],
- ];
- }
+ /**
+ * @dataProvider GetCredentialsProvider
+ */
+ public function testGetCredentials($aParameters, $aExpectedCredentials)
+ {
+ $oParametersMock = $this->createMock(\Parameters::class);
+ $oParametersMock->expects($this->atLeast(1))
+ ->method('Get')
+ ->will($this->returnCallback(
+ function ($sKey, $aDefaultValue) use ($aParameters) {
+ if (array_key_exists($sKey, $aParameters)) {
+ return $aParameters[$sKey];
+ }
+ return $aDefaultValue;
+ }
+ ));
- /**
- * @dataProvider GetCredentialsProvider
- */
- public function testGetCredentials($aParameters, $aExpectedCredentials){
- $oParametersMock = $this->createMock(\Parameters::class);
- $oParametersMock->expects($this->atLeast(1))
- ->method('Get')
- ->will($this->returnCallback(
- function($sKey, $aDefaultValue) use ($aParameters) {
- if (array_key_exists($sKey, $aParameters)){
- return $aParameters[$sKey];
- }
- return $aDefaultValue;
- }
- ));
+ $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
+ $reflection->setAccessible(true);
+ $reflection->setValue(null, $oParametersMock);
- $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
- $reflection->setAccessible(true);
- $reflection->setValue(null, $oParametersMock);
+ $this->assertEquals($aExpectedCredentials, Utils::GetCredentials());
+ }
- $this->assertEquals($aExpectedCredentials, Utils::GetCredentials());
- }
+ /**
+ * @dataProvider GetLoginModeProvider
+ */
+ public function testGetLoginForm($aParameters, $sExpectedLoginMode)
+ {
+ $oParametersMock = $this->createMock(\Parameters::class);
+ $oParametersMock->expects($this->atLeast(1))
+ ->method('Get')
+ ->will($this->returnCallback(
+ function ($sKey, $aDefaultValue) use ($aParameters) {
+ if (array_key_exists($sKey, $aParameters)) {
+ return $aParameters[$sKey];
+ }
+ return $aDefaultValue;
+ }
+ ));
- /**
- * @dataProvider GetLoginModeProvider
- */
- public function testGetLoginForm($aParameters, $sExpectedLoginMode){
- $oParametersMock = $this->createMock(\Parameters::class);
- $oParametersMock->expects($this->atLeast(1))
- ->method('Get')
- ->will($this->returnCallback(
- function($sKey, $aDefaultValue) use ($aParameters) {
- if (array_key_exists($sKey, $aParameters)){
- return $aParameters[$sKey];
- }
- return $aDefaultValue;
- }
- ));
+ $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
+ $reflection->setAccessible(true);
+ $reflection->setValue(null, $oParametersMock);
- $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
- $reflection->setAccessible(true);
- $reflection->setValue(null, $oParametersMock);
+ $this->assertEquals($sExpectedLoginMode, Utils::GetLoginMode());
+ }
- $this->assertEquals($sExpectedLoginMode, Utils::GetLoginMode());
- }
+ public function GetLoginModeProvider()
+ {
+ return [
+ 'login/password (nominal)' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2'
+ ],
+ 'sExpectedLoginMode' => 'form'
+ ],
+ 'authent-token v2' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_token' => 'admin4',
+ ],
+ 'sExpectedLoginMode' => 'token'
+ ],
+ 'new token over legacy one' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_rest_token' => 'admin3',
+ 'itop_token' => 'admin4',
+ ],
+ 'sExpectedLoginMode' => 'token'
+ ],
+ 'itop_login_mode over others' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_rest-token' => 'admin3',
+ 'itop_token' => 'admin4',
+ 'itop_login_mode' => 'newloginform',
+ ],
+ 'sExpectedLoginMode' => 'newloginform'
+ ],
+ ];
+ }
- public function GetLoginModeProvider(){
- return [
- 'login/password (nominal)' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2'
- ],
- 'sExpectedLoginMode' => 'form'
- ],
- 'authent-token v2' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_token' => 'admin4',
- ],
- 'sExpectedLoginMode' => 'token'
- ],
- 'new token over legacy one' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_rest_token' => 'admin3',
- 'itop_token' => 'admin4',
- ],
- 'sExpectedLoginMode' => 'token'
- ],
- 'itop_login_mode over others' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_rest-token' => 'admin3',
- 'itop_token' => 'admin4',
- 'itop_login_mode' => 'newloginform',
- ],
- 'sExpectedLoginMode' => 'newloginform'
- ],
- ];
- }
+ public function testDumpConfig()
+ {
+ global $argv;
+ $sXmlPath = __DIR__.'/utils/params.test.xml';
+ $argv[] = "--config_file=".$sXmlPath;
+ $sContent = file_get_contents($sXmlPath);
+ $this->assertEquals($sContent, Utils::DumpConfig());
+ }
- public function testDumpConfig(){
- global $argv;
- $sXmlPath = __DIR__.'/utils/params.test.xml';
- $argv[]= "--config_file=".$sXmlPath;
- $sContent = file_get_contents($sXmlPath);
- $this->assertEquals($sContent, Utils::DumpConfig());
- }
-
- public function testCheckModuleInstallation(){
- $oRestClient = $this->createMock(\RestClient::class);
-
- $oReflectionLastInstallDate = new \ReflectionProperty(Utils::class, 'sLastInstallDate');
- $oReflectionLastInstallDate->setAccessible(true);
- $oReflectionLastInstallDate->setValue(null,'0000-00-00 00:00:00');
-
- $oRestClient->expects($this->exactly(4))
- ->method('Get')
- ->willReturnMap([
- ['ModuleInstallation', ['name' => 'itop-structure', 'installed' => '0000-00-00 00:00:00'], 'name,version', 1, [
- 'code' => 0,
- 'objects' => ['ModuleInstallation::0' => ['fields' => ['name' => 'itop-structure', 'version' => '0.0.0']]],
- 'message' => 'Found: 1',
- ]],
- ['ModuleInstallation', ['name' => 'fake-module', 'installed' => '0000-00-00 00:00:00'], 'name,version', 1, [
- 'code' => 0,
- 'objects' => null,
- 'message' => 'Found: 0',
- ]],
- ]);
-
- $this->assertTrue(Utils::CheckModuleInstallation('itop-structure', false, $oRestClient));
- $this->assertTrue(Utils::CheckModuleInstallation('itop-structure/0.0.0', false, $oRestClient));
- $this->assertFalse(Utils::CheckModuleInstallation('itop-structure/1.2.3', false, $oRestClient));
- $this->assertFalse(Utils::CheckModuleInstallation('fake-module', false, $oRestClient));
- }
+ public function testCheckModuleInstallation()
+ {
+ $oRestClient = $this->createMock(\RestClient::class);
+
+ $oReflectionLastInstallDate = new \ReflectionProperty(Utils::class, 'sLastInstallDate');
+ $oReflectionLastInstallDate->setAccessible(true);
+ $oReflectionLastInstallDate->setValue(null, '0000-00-00 00:00:00');
+
+ $oRestClient->expects($this->exactly(4))
+ ->method('Get')
+ ->willReturnMap([
+ ['ModuleInstallation', ['name' => 'itop-structure', 'installed' => '0000-00-00 00:00:00'], 'name,version', 1, [
+ 'code' => 0,
+ 'objects' => ['ModuleInstallation::0' => ['fields' => ['name' => 'itop-structure', 'version' => '0.0.0']]],
+ 'message' => 'Found: 1',
+ ]],
+ ['ModuleInstallation', ['name' => 'fake-module', 'installed' => '0000-00-00 00:00:00'], 'name,version', 1, [
+ 'code' => 0,
+ 'objects' => null,
+ 'message' => 'Found: 0',
+ ]],
+ ]);
+
+ $this->assertTrue(Utils::CheckModuleInstallation('itop-structure', false, $oRestClient));
+ $this->assertTrue(Utils::CheckModuleInstallation('itop-structure/0.0.0', false, $oRestClient));
+ $this->assertFalse(Utils::CheckModuleInstallation('itop-structure/1.2.3', false, $oRestClient));
+ $this->assertFalse(Utils::CheckModuleInstallation('fake-module', false, $oRestClient));
+ }
}
diff --git a/test/bootstrap.inc.php b/test/bootstrap.inc.php
index 2fa9b73..334f17a 100755
--- a/test/bootstrap.inc.php
+++ b/test/bootstrap.inc.php
@@ -2,5 +2,3 @@
require_once(dirname(__FILE__).'/../approot.inc.php');
include __DIR__.'/../vendor/autoload.php'; // composer autoload
-
-
diff --git a/test/collectionplan/collectors_files/LegacyCollector.class.inc.php b/test/collectionplan/collectors_files/LegacyCollector.class.inc.php
index a0b96aa..d070492 100644
--- a/test/collectionplan/collectors_files/LegacyCollector.class.inc.php
+++ b/test/collectionplan/collectors_files/LegacyCollector.class.inc.php
@@ -2,11 +2,11 @@
class LegacyCollector extends Collector
{
- /**
- * @inheritDoc
- */
- public function CheckToLaunch(array $aOrchestratedCollectors): bool
- {
- return false;
- }
-}
\ No newline at end of file
+ /**
+ * @inheritDoc
+ */
+ public function CheckToLaunch(array $aOrchestratedCollectors): bool
+ {
+ return false;
+ }
+}
diff --git a/test/collectionplan/collectors_files/extensions/src/ExtendedCollector.class.inc.php b/test/collectionplan/collectors_files/extensions/src/ExtendedCollector.class.inc.php
index 99654df..89c201b 100644
--- a/test/collectionplan/collectors_files/extensions/src/ExtendedCollector.class.inc.php
+++ b/test/collectionplan/collectors_files/extensions/src/ExtendedCollector.class.inc.php
@@ -2,5 +2,4 @@
class ExtendedCollector extends Collector
{
-
-}
\ No newline at end of file
+}
diff --git a/test/collectionplan/collectors_files/src/StandardCollector.class.inc.php b/test/collectionplan/collectors_files/src/StandardCollector.class.inc.php
index 9b1acb8..bb56d45 100644
--- a/test/collectionplan/collectors_files/src/StandardCollector.class.inc.php
+++ b/test/collectionplan/collectors_files/src/StandardCollector.class.inc.php
@@ -2,5 +2,4 @@
class StandardCollector extends Collector
{
-
}
diff --git a/test/collectionplan/collectors_files/src/TestCollectionPlan.class.inc.php b/test/collectionplan/collectors_files/src/TestCollectionPlan.class.inc.php
index e81054c..1ee4c1b 100644
--- a/test/collectionplan/collectors_files/src/TestCollectionPlan.class.inc.php
+++ b/test/collectionplan/collectors_files/src/TestCollectionPlan.class.inc.php
@@ -3,4 +3,3 @@
class TestCollectionPlan extends CollectionPlan
{
}
-
diff --git a/test/collector/attribute_isnullified/iTopPersonCollector.class.inc.php b/test/collector/attribute_isnullified/iTopPersonCollector.class.inc.php
index eb87c65..03e0c80 100644
--- a/test/collector/attribute_isnullified/iTopPersonCollector.class.inc.php
+++ b/test/collector/attribute_isnullified/iTopPersonCollector.class.inc.php
@@ -2,35 +2,35 @@
class iTopPersonCollector extends Collector
{
- private $bFetched;
+ private $bFetched;
- protected function Fetch()
- {
- if (! $this->bFetched) {
- $this->bFetched = true;
- return [
- 'primary_key' => 1,
- 'first_name' => "isaac",
- 'name' => "asimov",
- 'org_id' => "Demo",
- 'phone' => null,
- 'mobile_phone' => "123456",
- 'employee_number' => "9998877665544",
- 'email' => "issac.asimov@function.io",
- 'function' => "writer",
- 'Status' => "Active",
- ];
- }
+ protected function Fetch()
+ {
+ if (! $this->bFetched) {
+ $this->bFetched = true;
+ return [
+ 'primary_key' => 1,
+ 'first_name' => "isaac",
+ 'name' => "asimov",
+ 'org_id' => "Demo",
+ 'phone' => null,
+ 'mobile_phone' => "123456",
+ 'employee_number' => "9998877665544",
+ 'email' => "issac.asimov@function.io",
+ 'function' => "writer",
+ 'Status' => "Active",
+ ];
+ }
- return null;
- }
-
- /**
- * {@inheritDoc}
- * @see Collector::AttributeIsOptional()
- */
- public function AttributeIsOptional($sAttCode)
- {
- return ($sAttCode === 'optional');
- }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see Collector::AttributeIsOptional()
+ */
+ public function AttributeIsOptional($sAttCode)
+ {
+ return ($sAttCode === 'optional');
+ }
}
diff --git a/test/collector/attribute_isnullified/main.php b/test/collector/attribute_isnullified/main.php
index 73c3833..c43fb8f 100644
--- a/test/collector/attribute_isnullified/main.php
+++ b/test/collector/attribute_isnullified/main.php
@@ -1,4 +1,5 @@
'Centreon collector',
- 'category' => 'collector',
+ __FILE__, // Path to the current file, all other file names are relative to the directory containing this file
+ 'centreon-collector/1.0.0',
+ [
+ // Identification
+ //
+ 'label' => 'Centreon collector',
+ 'category' => 'collector',
- // Setup
- //
- 'dependencies' => array(),
- 'mandatory' => false,
- 'visible' => false,
+ // Setup
+ //
+ 'dependencies' => [],
+ 'mandatory' => false,
+ 'visible' => false,
- // Components
- //
- 'datamodel' => array(),
- 'webservice' => array(),
- 'data.struct' => array(// add your 'structure' definition XML files here,
- ),
- 'data.sample' => array(// add your sample data XML files here,
- ),
+ // Components
+ //
+ 'datamodel' => [],
+ 'webservice' => [],
+ 'data.struct' => [// add your 'structure' definition XML files here,
+ ],
+ 'data.sample' => [// add your sample data XML files here,
+ ],
- // Documentation
- //
- 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
- 'doc.more_information' => '', // hyperlink to more information, if any
+ // Documentation
+ //
+ 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
+ 'doc.more_information' => '', // hyperlink to more information, if any
- // Default settings
- //
- 'settings' => array(// Module specific settings go here, if any
- ),
- )
+ // Default settings
+ //
+ 'settings' => [// Module specific settings go here, if any
+ ],
+ ]
);
diff --git a/test/single_csv/common/main.php b/test/single_csv/common/main.php
index 3554aef..0fad150 100644
--- a/test/single_csv/common/main.php
+++ b/test/single_csv/common/main.php
@@ -1,4 +1,5 @@
CheckDataSourceDefinition($aExpectedSourceDefinition);
+ public function testDataSourcesAreEquivalent($aPlaceholders)
+ {
+ $bResult = true;
+ $sJSONSourceDefinition = file_get_contents(APPROOT."/collectors/ITopPersonJsonCollector.json");
+ $aExpectedSourceDefinition = json_decode($sJSONSourceDefinition, true);
+ $this->CheckDataSourceDefinition($aExpectedSourceDefinition);
- $sJSONExpectedDefinition = file_get_contents(APPROOT."/collectors/ITopPersonJsonSourceCollector.json");
- $aCurrentSourceDefinition = json_decode($sJSONExpectedDefinition, true);
+ $sJSONExpectedDefinition = file_get_contents(APPROOT."/collectors/ITopPersonJsonSourceCollector.json");
+ $aCurrentSourceDefinition = json_decode($sJSONExpectedDefinition, true);
- if ($this->DataSourcesAreEquivalent($aExpectedSourceDefinition, $aCurrentSourceDefinition)) {
- Utils::Log(LOG_INFO, "Ok, the Synchro Data Source exists in iTop and is up to date");
- } else {
- Utils::Log(LOG_INFO, "The Synchro Data Source definition for must be updated in iTop.");
- }
+ if ($this->DataSourcesAreEquivalent($aExpectedSourceDefinition, $aCurrentSourceDefinition)) {
+ Utils::Log(LOG_INFO, "Ok, the Synchro Data Source exists in iTop and is up to date");
+ } else {
+ Utils::Log(LOG_INFO, "The Synchro Data Source definition for must be updated in iTop.");
+ }
- return $bResult;
- }
+ return $bResult;
+ }
}
diff --git a/test/single_json/common/main.php b/test/single_json/common/main.php
index bd832d5..eb95845 100644
--- a/test/single_json/common/main.php
+++ b/test/single_json/common/main.php
@@ -1,7 +1,8 @@
Get('SynchroDataSource', 'SELECT SynchroDataSource');
- $sITopUrl = Utils::GetConfigurationValue('itop_url', '');
- if ($aResult['code'] != 0) {
- echo "[{$aResult['code']}] {$aResult['message']}\n";
- exit - 1;
- }
+ // List all tasks defined in iTop
+ $oRestClient = new RestClient();
+ $aResult = $oRestClient->Get('SynchroDataSource', 'SELECT SynchroDataSource');
+ $sITopUrl = Utils::GetConfigurationValue('itop_url', '');
+ if ($aResult['code'] != 0) {
+ echo "[{$aResult['code']}] {$aResult['message']}\n";
+ exit - 1;
+ }
- echo "iTop: \033[34m{$sITopUrl}\033[0m\n";
- if (empty($aResult['objects'])) {
- echo " no SynchroDataSource defined\n";
- } else {
- switch (count($aResult['objects'])) {
- case 1:
- echo " 1 SynchroDataSource defined\n";
- break;
+ echo "iTop: \033[34m{$sITopUrl}\033[0m\n";
+ if (empty($aResult['objects'])) {
+ echo " no SynchroDataSource defined\n";
+ } else {
+ switch (count($aResult['objects'])) {
+ case 1:
+ echo " 1 SynchroDataSource defined\n";
+ break;
- default:
- echo " ".count($aResult['objects'])." SynchroDataSource objects defined\n";
- break;
- }
+ default:
+ echo " ".count($aResult['objects'])." SynchroDataSource objects defined\n";
+ break;
+ }
- echo "+-----+--------------------------------------------------------------------------------------+\n";
- echo "| Id | Name |\n";
- echo "+-----+--------------------------------------------------------------------------------------+\n";
- foreach ($aResult['objects'] as $aValues) {
- $aCurrentTaskDefinition = $aValues['fields'];
- echo sprintf("| %3d | %-84.84s |\n",
- $aValues['key'],
- $aCurrentTaskDefinition['name'],
- );
- }
- echo "+-----+--------------------------------------------------------------------------------------+\n";
- }
- $sMaxVersion = $oRestClient->GetNewestKnownVersion();
- echo "iTop REST/API version: $sMaxVersion\n";
+ echo "+-----+--------------------------------------------------------------------------------------+\n";
+ echo "| Id | Name |\n";
+ echo "+-----+--------------------------------------------------------------------------------------+\n";
+ foreach ($aResult['objects'] as $aValues) {
+ $aCurrentTaskDefinition = $aValues['fields'];
+ echo sprintf(
+ "| %3d | %-84.84s |\n",
+ $aValues['key'],
+ $aCurrentTaskDefinition['name'],
+ );
+ }
+ echo "+-----+--------------------------------------------------------------------------------------+\n";
+ }
+ $sMaxVersion = $oRestClient->GetNewestKnownVersion();
+ echo "iTop REST/API version: $sMaxVersion\n";
} else {
- // Generate the pretty-printed JSON representation of the specified task
- $oRestClient = new RestClient();
- $aResult = $oRestClient->Get('SynchroDataSource', array('name' => $sTaskName), '*');
- if ($aResult['code'] != 0) {
- echo "Sorry, an error occurred while retrieving the information from iTop: {$aResult['message']} ({$aResult['code']})\n";
- } else {
- if (is_array($aResult['objects']) && (count($aResult['objects']) > 0)) {
- foreach ($aResult['objects'] as $sKey => $aValues) {
- $iKey=0;
- if (!array_key_exists('key', $aValues)) {
- // Emulate the behavior for older versions of the API
- if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
- $iKey = (int)$aMatches[1];
- }
- } else {
- $iKey = (int)$aValues['key'];
- }
- $aCurrentTaskDefinition = $aValues['fields'];
- RestClient::GetFullSynchroDataSource($aCurrentTaskDefinition, $iKey);
+ // Generate the pretty-printed JSON representation of the specified task
+ $oRestClient = new RestClient();
+ $aResult = $oRestClient->Get('SynchroDataSource', ['name' => $sTaskName], '*');
+ if ($aResult['code'] != 0) {
+ echo "Sorry, an error occurred while retrieving the information from iTop: {$aResult['message']} ({$aResult['code']})\n";
+ } else {
+ if (is_array($aResult['objects']) && (count($aResult['objects']) > 0)) {
+ foreach ($aResult['objects'] as $sKey => $aValues) {
+ $iKey = 0;
+ if (!array_key_exists('key', $aValues)) {
+ // Emulate the behavior for older versions of the API
+ if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
+ $iKey = (int)$aMatches[1];
+ }
+ } else {
+ $iKey = (int)$aValues['key'];
+ }
+ $aCurrentTaskDefinition = $aValues['fields'];
+ RestClient::GetFullSynchroDataSource($aCurrentTaskDefinition, $iKey);
- // Replace some literals by their usual placeholders
- $aCurrentTaskDefinition['user_id'] = '$synchro_user$';
- $aCurrentTaskDefinition['notify_contact_id'] = '$contact_to_notify$';
+ // Replace some literals by their usual placeholders
+ $aCurrentTaskDefinition['user_id'] = '$synchro_user$';
+ $aCurrentTaskDefinition['notify_contact_id'] = '$contact_to_notify$';
- echo json_encode($aCurrentTaskDefinition, JSON_PRETTY_PRINT);
- }
- } else {
- echo "Sorry, no SynchroDataSource named '$sTaskName' found in iTop.\n";
- }
- }
+ echo json_encode($aCurrentTaskDefinition, JSON_PRETTY_PRINT);
+ }
+ } else {
+ echo "Sorry, no SynchroDataSource named '$sTaskName' found in iTop.\n";
+ }
+ }
}
diff --git a/toolkit/testconnection.php b/toolkit/testconnection.php
index 9bc81b6..36f430a 100644
--- a/toolkit/testconnection.php
+++ b/toolkit/testconnection.php
@@ -8,14 +8,13 @@
print ' curl_init exists: '.function_exists('curl_init').PHP_EOL;
try {
- Utils::InitConsoleLogLevel();
+ Utils::InitConsoleLogLevel();
- $oRestClient = new RestClient();
- var_dump($oRestClient->ListOperations());
- print 'Calling iTop Rest API worked!'.PHP_EOL;
- exit(0);
-}
-catch (Exception $e) {
- print $e->getMessage().PHP_EOL;
- exit(1);
+ $oRestClient = new RestClient();
+ var_dump($oRestClient->ListOperations());
+ print 'Calling iTop Rest API worked!'.PHP_EOL;
+ exit(0);
+} catch (Exception $e) {
+ print $e->getMessage().PHP_EOL;
+ exit(1);
}
From 39771f5351ef94fc3e0169ea226e29002bea4076 Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 08:53:50 +0200
Subject: [PATCH 15/20] document validate.sh
---
test/php-code-style/README.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/test/php-code-style/README.md b/test/php-code-style/README.md
index b33fadb..401cdc7 100644
--- a/test/php-code-style/README.md
+++ b/test/php-code-style/README.md
@@ -17,4 +17,10 @@ to respect iTop code standards and re-format (no path provided means whole iTop
```
test/php-code-style/vendor/bin/php-cs-fixer fix --config test/php-code-style/.php-cs-fixer.dist.php
+```
+
+report generated in Jenkins is in verbose. you can have it by calling below CLI:
+
+```
+test/php-code-style/validate.sh
```
\ No newline at end of file
From 5a2e74e4fb8eecaecfa4f27bae7a26c80a8433d5 Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 11:33:05 +0200
Subject: [PATCH 16/20] indent with tabs + inception style applied everywhere
even php-cs-fixer itself
---
test/php-code-style/.php-cs-fixer.dist.php | 23 ++++++++++++----------
1 file changed, 13 insertions(+), 10 deletions(-)
diff --git a/test/php-code-style/.php-cs-fixer.dist.php b/test/php-code-style/.php-cs-fixer.dist.php
index a3cf523..81fa0c6 100644
--- a/test/php-code-style/.php-cs-fixer.dist.php
+++ b/test/php-code-style/.php-cs-fixer.dist.php
@@ -1,19 +1,22 @@
exclude('vendor')
+ ->exclude('vendor')
->in($APPROOT)
- ;
+;
$config = new PhpCsFixer\Config();
return $config->setRiskyAllowed(true)
- ->setRules([
- '@PSR12' => true,
- 'no_extra_blank_lines' => true,
- 'array_syntax' => ['syntax' => 'short'],
- ])
- ->setFinder($finder)
-;
\ No newline at end of file
+ ->setRules([
+ '@PSR12' => true,
+ 'indentation_type' => true,
+ 'no_extra_blank_lines' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ ])
+ ->setIndent("\t")
+ ->setLineEnding("\n")
+ ->setFinder($finder)
+;
From 15a8f5c79b91c587fef71d085621ff4af0af9d74 Mon Sep 17 00:00:00 2001
From: odain
Date: Fri, 17 Oct 2025 11:33:22 +0200
Subject: [PATCH 17/20] indent with tabs
---
core/callitopservice.class.inc.php | 26 +-
core/collectionplan.class.inc.php | 260 +-
core/collector.class.inc.php | 2228 ++++++++---------
core/csvcollector.class.inc.php | 716 +++---
core/dopostrequestservice.class.inc.php | 8 +-
core/jsoncollector.class.inc.php | 652 ++---
core/lookuptable.class.inc.php | 336 +--
core/mappingtable.class.inc.php | 106 +-
core/orchestrator.class.inc.php | 532 ++--
core/parameters.class.inc.php | 422 ++--
core/polyfill.inc.php | 20 +-
core/restclient.class.inc.php | 378 +--
core/sqlcollector.class.inc.php | 392 +--
core/utils.class.inc.php | 1446 +++++------
exec.php | 160 +-
test/CallItopServiceTest.php | 142 +-
test/CollectionPlanTest.php | 260 +-
test/CollectorSynchroTest.php | 318 +--
test/CollectorTest.php | 392 +--
test/CsvCollectorTest.php | 388 +--
test/FakeCollector.php | 12 +-
test/JsonCollectorTest.php | 618 ++---
test/LookupTest.php | 440 ++--
test/OrchestratorTest.php | 140 +-
test/ParametersTest.php | 80 +-
test/RestTest.php | 186 +-
test/UtilsTest.php | 466 ++--
.../LegacyCollector.class.inc.php | 14 +-
.../iTopPersonCollector.class.inc.php | 56 +-
test/getproject/module/module.myproject.php | 58 +-
.../iTopPersonCsvCollector.class.inc.php | 8 +-
.../iTopPersonCsvCollector.class.inc.php | 8 +-
.../iTopPersonCsvCollector.class.inc.php | 8 +-
.../iTopPersonCsvCollector.class.inc.php | 8 +-
.../iTopPersonCsvCollector.class.inc.php | 8 +-
.../iTopPersonCsvCollector.class.inc.php | 8 +-
.../ITopPersonJsonCollector.class.inc.php | 44 +-
toolkit/dump_tasks.php | 132 +-
toolkit/testconnection.php | 14 +-
39 files changed, 5745 insertions(+), 5745 deletions(-)
diff --git a/core/callitopservice.class.inc.php b/core/callitopservice.class.inc.php
index af8500a..919b0ba 100644
--- a/core/callitopservice.class.inc.php
+++ b/core/callitopservice.class.inc.php
@@ -5,21 +5,21 @@
*/
class CallItopService
{
- public function CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut = -1)
- {
+ public function CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut = -1)
+ {
- $sUrl = Utils::GetConfigurationValue('itop_url', '').$sUri;
+ $sUrl = Utils::GetConfigurationValue('itop_url', '').$sUri;
- $aData = array_merge(
- Utils::GetCredentials(),
- $aAdditionalData
- );
+ $aData = array_merge(
+ Utils::GetCredentials(),
+ $aAdditionalData
+ );
- // timeout in seconds, for a synchro to run
- $iCurrentTimeOut = ($iTimeOut === -1) ? (int)Utils::GetConfigurationValue('itop_synchro_timeout', 600) : $iTimeOut;
- $aCurlOptions = Utils::GetCurlOptions($iCurrentTimeOut);
+ // timeout in seconds, for a synchro to run
+ $iCurrentTimeOut = ($iTimeOut === -1) ? (int)Utils::GetConfigurationValue('itop_synchro_timeout', 600) : $iTimeOut;
+ $aCurlOptions = Utils::GetCurlOptions($iCurrentTimeOut);
- $aResponseHeaders = null;
- return Utils::DoPostRequest($sUrl, $aData, '', $aResponseHeaders, $aCurlOptions);
- }
+ $aResponseHeaders = null;
+ return Utils::DoPostRequest($sUrl, $aData, '', $aResponseHeaders, $aCurlOptions);
+ }
}
diff --git a/core/collectionplan.class.inc.php b/core/collectionplan.class.inc.php
index 0249d1f..2522219 100644
--- a/core/collectionplan.class.inc.php
+++ b/core/collectionplan.class.inc.php
@@ -21,135 +21,135 @@
*/
abstract class CollectionPlan
{
- // Instance of the collection plan
- protected static $oCollectionPlan;
-
- public function __construct()
- {
- self::$oCollectionPlan = $this;
- }
-
- /**
- * Initialize collection plan
- *
- * @return void
- * @throws \IOException
- */
- public function Init(): void
- {
- Utils::Log(LOG_INFO, "---------- Build collection plan ----------");
- }
-
- /**
- * @return static
- */
- public static function GetPlan()
- {
- return self::$oCollectionPlan;
- }
-
- /**
- * Provide the launch sequence as defined in the configuration files
- *
- * @return array
- * @throws \Exception
- */
- public function GetSortedLaunchSequence(): array
- {
- $aCollectorsLaunchSequence = Utils::GetConfigurationValue('collectors_launch_sequence', []);
- $aExtensionsCollectorsLaunchSequence = Utils::GetConfigurationValue('extensions_collectors_launch_sequence', []);
- $aCollectorsLaunchSequence = array_merge($aCollectorsLaunchSequence, $aExtensionsCollectorsLaunchSequence);
- $aRank = [];
- if (!empty($aCollectorsLaunchSequence)) {
- // Sort sequence
- $aSortedCollectorsLaunchSequence = [];
- foreach ($aCollectorsLaunchSequence as $aCollector) {
- if (array_key_exists('rank', $aCollector)) {
- $aRank[] = $aCollector['rank'];
- $aSortedCollectorsLaunchSequence[] = $aCollector;
- } else {
- Utils::Log(LOG_INFO, "> Rank is missing from the launch_sequence of ".$aCollector['name']." It will not be launched.");
- }
- }
- array_multisort($aRank, SORT_ASC, $aSortedCollectorsLaunchSequence);
-
- return $aSortedCollectorsLaunchSequence;
- }
-
- return $aCollectorsLaunchSequence;
- }
-
- /**
- * Look for the collector definition file in the different possible collector directories
- *
- * @param $sCollector
- *
- * @return bool
- */
- public function GetCollectorDefinitionFile($sCollector): bool
- {
- if (file_exists(APPROOT.'collectors/extensions/src/'.$sCollector.'.class.inc.php')) {
- require_once(APPROOT.'collectors/extensions/src/'.$sCollector.'.class.inc.php');
- } elseif (file_exists(APPROOT.'collectors/src/'.$sCollector.'.class.inc.php')) {
- require_once(APPROOT.'collectors/src/'.$sCollector.'.class.inc.php');
- } elseif (file_exists(APPROOT.'collectors/'.$sCollector.'.class.inc.php')) {
- require_once(APPROOT.'collectors/'.$sCollector.'.class.inc.php');
- } else {
- return false;
- }
-
- return true;
- }
-
- /**
- * Add the collectors to be launched to the orchestrator
- *
- * @return bool
- * @throws \Exception
- */
- public function AddCollectorsToOrchestrator(): bool
- {
- // Read and order launch sequence
- $aCollectorsLaunchSequence = $this->GetSortedLaunchSequence();
- if (empty($aCollectorsLaunchSequence)) {
- Utils::Log(LOG_INFO, "---------- No Launch sequence has been found, no collector has been orchestrated ----------");
-
- return false;
- }
-
- $iIndex = 1;
- $aOrchestratedCollectors = [];
- foreach ($aCollectorsLaunchSequence as $iKey => $aCollector) {
- $sCollectorName = $aCollector['name'];
-
- // Skip disabled collectors
- if (!array_key_exists('enable', $aCollector) || ($aCollector['enable'] != 'yes')) {
- Utils::Log(LOG_INFO, "> ".$sCollectorName." is disabled and will not be launched.");
- continue;
- }
-
- // Read collector php definition file
- if (!$this->GetCollectorDefinitionFile($sCollectorName)) {
- Utils::Log(LOG_INFO, "> No file definition file has been found for ".$sCollectorName." It will not be launched.");
- continue;
- }
-
- /** @var Collector $oCollector */
- // Instantiate collector
- $oCollector = new $sCollectorName();
- $oCollector->Init();
- if ($oCollector->CheckToLaunch($aOrchestratedCollectors)) {
- Utils::Log(LOG_INFO, $sCollectorName.' will be launched !');
- Orchestrator::AddCollector($iIndex++, $sCollectorName);
- $aOrchestratedCollectors[$sCollectorName] = true;
- } else {
- $aOrchestratedCollectors[$sCollectorName] = false;
- }
- unset($oCollector);
- }
- Utils::Log(LOG_INFO, "---------- Collectors have been orchestrated ----------");
-
- return true;
- }
+ // Instance of the collection plan
+ protected static $oCollectionPlan;
+
+ public function __construct()
+ {
+ self::$oCollectionPlan = $this;
+ }
+
+ /**
+ * Initialize collection plan
+ *
+ * @return void
+ * @throws \IOException
+ */
+ public function Init(): void
+ {
+ Utils::Log(LOG_INFO, "---------- Build collection plan ----------");
+ }
+
+ /**
+ * @return static
+ */
+ public static function GetPlan()
+ {
+ return self::$oCollectionPlan;
+ }
+
+ /**
+ * Provide the launch sequence as defined in the configuration files
+ *
+ * @return array
+ * @throws \Exception
+ */
+ public function GetSortedLaunchSequence(): array
+ {
+ $aCollectorsLaunchSequence = Utils::GetConfigurationValue('collectors_launch_sequence', []);
+ $aExtensionsCollectorsLaunchSequence = Utils::GetConfigurationValue('extensions_collectors_launch_sequence', []);
+ $aCollectorsLaunchSequence = array_merge($aCollectorsLaunchSequence, $aExtensionsCollectorsLaunchSequence);
+ $aRank = [];
+ if (!empty($aCollectorsLaunchSequence)) {
+ // Sort sequence
+ $aSortedCollectorsLaunchSequence = [];
+ foreach ($aCollectorsLaunchSequence as $aCollector) {
+ if (array_key_exists('rank', $aCollector)) {
+ $aRank[] = $aCollector['rank'];
+ $aSortedCollectorsLaunchSequence[] = $aCollector;
+ } else {
+ Utils::Log(LOG_INFO, "> Rank is missing from the launch_sequence of ".$aCollector['name']." It will not be launched.");
+ }
+ }
+ array_multisort($aRank, SORT_ASC, $aSortedCollectorsLaunchSequence);
+
+ return $aSortedCollectorsLaunchSequence;
+ }
+
+ return $aCollectorsLaunchSequence;
+ }
+
+ /**
+ * Look for the collector definition file in the different possible collector directories
+ *
+ * @param $sCollector
+ *
+ * @return bool
+ */
+ public function GetCollectorDefinitionFile($sCollector): bool
+ {
+ if (file_exists(APPROOT.'collectors/extensions/src/'.$sCollector.'.class.inc.php')) {
+ require_once(APPROOT.'collectors/extensions/src/'.$sCollector.'.class.inc.php');
+ } elseif (file_exists(APPROOT.'collectors/src/'.$sCollector.'.class.inc.php')) {
+ require_once(APPROOT.'collectors/src/'.$sCollector.'.class.inc.php');
+ } elseif (file_exists(APPROOT.'collectors/'.$sCollector.'.class.inc.php')) {
+ require_once(APPROOT.'collectors/'.$sCollector.'.class.inc.php');
+ } else {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Add the collectors to be launched to the orchestrator
+ *
+ * @return bool
+ * @throws \Exception
+ */
+ public function AddCollectorsToOrchestrator(): bool
+ {
+ // Read and order launch sequence
+ $aCollectorsLaunchSequence = $this->GetSortedLaunchSequence();
+ if (empty($aCollectorsLaunchSequence)) {
+ Utils::Log(LOG_INFO, "---------- No Launch sequence has been found, no collector has been orchestrated ----------");
+
+ return false;
+ }
+
+ $iIndex = 1;
+ $aOrchestratedCollectors = [];
+ foreach ($aCollectorsLaunchSequence as $iKey => $aCollector) {
+ $sCollectorName = $aCollector['name'];
+
+ // Skip disabled collectors
+ if (!array_key_exists('enable', $aCollector) || ($aCollector['enable'] != 'yes')) {
+ Utils::Log(LOG_INFO, "> ".$sCollectorName." is disabled and will not be launched.");
+ continue;
+ }
+
+ // Read collector php definition file
+ if (!$this->GetCollectorDefinitionFile($sCollectorName)) {
+ Utils::Log(LOG_INFO, "> No file definition file has been found for ".$sCollectorName." It will not be launched.");
+ continue;
+ }
+
+ /** @var Collector $oCollector */
+ // Instantiate collector
+ $oCollector = new $sCollectorName();
+ $oCollector->Init();
+ if ($oCollector->CheckToLaunch($aOrchestratedCollectors)) {
+ Utils::Log(LOG_INFO, $sCollectorName.' will be launched !');
+ Orchestrator::AddCollector($iIndex++, $sCollectorName);
+ $aOrchestratedCollectors[$sCollectorName] = true;
+ } else {
+ $aOrchestratedCollectors[$sCollectorName] = false;
+ }
+ unset($oCollector);
+ }
+ Utils::Log(LOG_INFO, "---------- Collectors have been orchestrated ----------");
+
+ return true;
+ }
}
diff --git a/core/collector.class.inc.php b/core/collector.class.inc.php
index 4a14e08..06691f4 100644
--- a/core/collector.class.inc.php
+++ b/core/collector.class.inc.php
@@ -36,1119 +36,1119 @@ class InvalidConfigException extends Exception
*/
abstract class Collector
{
- /**
- * @see N°2417
- * @var string TABLENAME_PATTERN used to validate data synchro table name
- */
- public const TABLENAME_PATTERN = '/^[A-Za-z0-9_]*$/';
-
- protected $sProjectName;
- protected $sSynchroDataSourceDefinitionFile;
- protected $sVersion;
- protected $iSourceId;
- protected $aFields;
- protected $aCSVHeaders;
- protected $aCSVFile;
- protected $iFileIndex;
- protected $aCollectorConfig;
- protected $sErrorMessage;
- protected $sSeparator;
- protected $aSkippedAttributes;
- protected $aNullifiedAttributes;
- protected $sSourceName;
-
- /** @var \CallItopService $oCallItopService */
- protected static $oCallItopService;
-
- /**
- * Construction
- */
- public function __construct()
- {
- $this->sVersion = null;
- $this->iSourceId = null;
- $this->aFields = [];
- $this->aCSVHeaders = [];
- $this->aCSVFile = [];
- $this->iFileIndex = null;
- $this->aCollectorConfig = [];
- $this->sErrorMessage = '';
- $this->sSeparator = ';';
- $this->aSkippedAttributes = [];
- }
-
- /**
- * @since 1.3.0
- */
- public static function SetCallItopService(CallItopService $oCurrentCallItopService)
- {
- static::$oCallItopService = $oCurrentCallItopService;
- }
-
- /**
- * Initialization
- *
- * @return void
- * @throws \Exception
- */
- public function Init(): void
- {
- $sJSONSourceDefinition = $this->GetSynchroDataSourceDefinition();
- if (empty($sJSONSourceDefinition)) {
- Utils::Log(
- LOG_ERR,
- sprintf(
- "Empty Synchro Data Source definition for the collector '%s' (file to check/create: %s)",
- $this->GetName(),
- $this->sSynchroDataSourceDefinitionFile
- )
- );
- throw new Exception('Cannot create Collector (empty JSON definition)');
- }
- $aSourceDefinition = json_decode($sJSONSourceDefinition, true);
-
- if ($aSourceDefinition === null) {
- Utils::Log(LOG_ERR, "Invalid Synchro Data Source definition for the collector '".$this->GetName()."' (not a JSON string)");
- throw new Exception('Cannot create Collector (invalid JSON definition)');
- }
- foreach ($aSourceDefinition['attribute_list'] as $aAttr) {
- $aColumns = isset($aAttr['import_columns']) ? explode(',', $aAttr['import_columns']) : [$aAttr['attcode']];
- $this->aFields[$aAttr['attcode']] = ['class' => $aAttr['finalclass'], 'update' => ($aAttr['update'] != 0), 'reconcile' => ($aAttr['reconcile'] != 0), 'columns' => $aColumns];
- }
-
- $this->ReadCollectorConfig();
- if (array_key_exists('nullified_attributes', $this->aCollectorConfig)) {
- $this->aNullifiedAttributes = $this->aCollectorConfig['nullified_attributes'];
- } else {
- $this->aNullifiedAttributes = Utils::GetConfigurationValue(get_class($this)."_nullified_attributes", null);
-
- if ($this->aNullifiedAttributes === null) {
- // Try all lowercase
- $this->aNullifiedAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_nullified_attributes", []);
- }
- }
- }
-
- public function ReadCollectorConfig()
- {
- $this->aCollectorConfig = Utils::GetConfigurationValue(get_class($this), []);
- if (empty($this->aCollectorConfig)) {
- $this->aCollectorConfig = Utils::GetConfigurationValue(strtolower(get_class($this)), []);
- }
- Utils::Log(
- LOG_DEBUG,
- sprintf(
- "aCollectorConfig %s: [%s]",
- get_class($this),
- json_encode($this->aCollectorConfig)
- )
- );
- }
-
- public function GetErrorMessage()
- {
- return $this->sErrorMessage;
- }
-
- public function GetProjectName()
- {
- return $this->sProjectName;
- }
-
- protected function Fetch()
- {
- // Implement your own mechanism, unless you completely overload Collect()
- }
-
- protected function Prepare()
- {
- $this->RemoveDataFiles();
- $this->sSeparator = ';';
-
- return true;
- }
-
- protected function Cleanup()
- {
- if ($this->iFileIndex !== null) {
- fclose($this->aCSVFile[$this->iFileIndex]);
- }
- }
-
- /*
- * Look for the synchro data source definition file in the different possible collector directories
- *
- * @return false|string
- */
- public function GetSynchroDataSourceDefinitionFile()
- {
- if (file_exists(APPROOT.'collectors/extensions/json/'.get_class($this).'.json')) {
- return APPROOT.'collectors/extensions/json/'.get_class($this).'.json';
- } elseif (file_exists(APPROOT.'collectors/json/'.get_class($this).'.json')) {
- return APPROOT.'collectors/json/'.get_class($this).'.json';
- } elseif (file_exists(APPROOT.'collectors/'.get_class($this).'.json')) {
- return APPROOT.'collectors/'.get_class($this).'.json';
- } else {
- return false;
- }
- }
-
- public function GetSynchroDataSourceDefinition($aPlaceHolders = [])
- {
- $this->sSynchroDataSourceDefinitionFile = $this->GetSynchroDataSourceDefinitionFile();
- if ($this->sSynchroDataSourceDefinitionFile === false) {
- return false;
- }
-
- $aPlaceHolders['$version$'] = $this->GetVersion();
- $sSynchroDataSourceDefinition = file_get_contents($this->sSynchroDataSourceDefinitionFile);
- $sSynchroDataSourceDefinition = str_replace(array_keys($aPlaceHolders), array_values($aPlaceHolders), $sSynchroDataSourceDefinition);
-
- return $sSynchroDataSourceDefinition;
- }
-
- /**
- * Determine if a given attribute can be missing in the data datamodel.
- *
- * Overload this method to let your collector adapt to various datamodels. If an attribute is skipped,
- * its name is recorded in the member variable $this->aSkippedAttributes for later reference.
- *
- * @param string $sAttCode
- *
- * @return boolean True if the attribute can be skipped, false otherwise
- */
- public function AttributeIsOptional($sAttCode)
- {
- return false; // By default no attribute is optional
- }
-
- /**
- * Determine if a given attribute null value is allowed to be transformed between collect and data synchro steps.
- *
- * If transformed, its null value is replaced by '' and sent to iTop data synchro.
- * It means that existing value on iTop side will be kept as is.
- *
- * Otherwise if not transformed, empty string value will be sent to datasynchro which means resetting current value on iTop side.
- *
- * Best practice:
- * for fields like decimal, integer or enum, we recommend to configure transformation as resetting will fail on iTop side (Bug N°776).
- *
- * The implementation is based on a predefined configuration parameter named from the
- * class of the collector (all lowercase) with _nullified_attributes appended.
- *
- * Example: here is the configuration to "nullify" the attribute 'location_id' for the class MyCollector:
- *
- * location_id
- *
- *
- * @param string $sAttCode
- *
- * @return boolean True if the attribute can be skipped, false otherwise
- */
- public function AttributeIsNullified($sAttCode)
- {
- if (is_array($this->aNullifiedAttributes)) {
- return in_array($sAttCode, $this->aNullifiedAttributes);
- }
-
- return false;
- }
-
- public function GetName()
- {
- return get_class($this);
- }
-
- public function GetVersion()
- {
- if ($this->sVersion == null) {
- $this->GetVersionFromModuleFile();
- }
-
- return $this->sVersion;
- }
-
- /**
- * Overload this method (to return true) if the collector has
- * to reprocess the CSV file (with an access to iTop)
- * before executing the synchro with iTop
- *
- * @return bool
- */
- protected function MustProcessBeforeSynchro()
- {
- // Overload this method (to return true) if the collector has
- // to reprocess the CSV file (with an access to iTop)
- // before executing the synchro with iTop
- return false;
- }
-
- /**
- * Overload this method to perform any one-time initialization which
- * may be required before processing the CSV file line by line
- *
- * @return void
- */
- protected function InitProcessBeforeSynchro()
- {
- }
-
- /**
- * Overload this method to process each line of the CSV file
- * Should you need to "reject" the line from the ouput, throw an exception of class IgnoredRowException
- * Example:
- * throw new IgnoredRowException('Explain why the line is rejected - visible in the debug output');
- *
- * @param array $aLineData
- * @param int $iLineIndex
- *
- * @return void
- * @throws IgnoredRowException
- */
- protected function ProcessLineBeforeSynchro(&$aLineData, $iLineIndex)
- {
- }
-
- protected function DoProcessBeforeSynchro()
- {
- $this->InitProcessBeforeSynchro();
-
- $aFiles = glob(Utils::GetDataFilePath(get_class($this).'.raw-*.csv'));
- foreach ($aFiles as $sDataFile) {
- Utils::Log(LOG_INFO, "Processing '$sDataFile'...");
- // Warning backslashes inside the file path (like C:\ on Windows) must be escaped (i.e. \ => \\), but inside a PHP string \ is written '\\' so \\ becomes '\\\\' !!
- $sPattern = '|'.str_replace('\\', '\\\\', Utils::GetDataFilePath(get_class($this))).'\\.raw-([0-9]+)\\.csv$|';
- if (preg_match($sPattern, $sDataFile, $aMatches)) {
- $idx = $aMatches[1];
- $sOutputFile = Utils::GetDataFilePath(get_class($this).'-'.$idx.'.csv');
- Utils::Log(LOG_DEBUG, "Converting '$sDataFile' to '$sOutputFile'...");
-
- $hCSV = fopen($sDataFile, 'r');
- if ($hCSV === false) {
- Utils::Log(LOG_ERR, "Failed to open '$sDataFile' for reading... file will be skipped.");
- }
-
- $hOutputCSV = fopen($sOutputFile, 'w');
- if ($hOutputCSV === false) {
- Utils::Log(LOG_ERR, "Failed to open '$sOutputFile' for writing... file will be skipped.");
- }
-
- if (($hCSV !== false) && ($hOutputCSV !== false)) {
- $iLineIndex = 0;
- while (($aData = fgetcsv($hCSV, 10000, $this->sSeparator)) !== false) {
- //process
- try {
- $this->ProcessLineBeforeSynchro($aData, $iLineIndex);
- // Write the CSV data
- fputcsv($hOutputCSV, $aData, $this->sSeparator);
- } catch (IgnoredRowException $e) {
- // Skip this line
- Utils::Log(LOG_DEBUG, "Ignoring the line $iLineIndex. Reason: ".$e->getMessage());
- }
- $iLineIndex++;
- }
- fclose($hCSV);
- fclose($hOutputCSV);
- Utils::Log(LOG_INFO, "End of processing of '$sDataFile'...");
- }
- } else {
- Utils::Log(LOG_DEBUG, "'$sDataFile' does not match '$sPattern'... file will be skipped.");
- }
- }
- }
-
- /**
- * Overload this method if the data collected is in a different character set
- *
- * @return string The name of the character set in which the collected data are encoded
- */
- protected function GetCharset()
- {
- return 'UTF-8';
- }
-
- /////////////////////////////////////////////////////////////////////////
-
- /**
- * Extracts the version number by evaluating the content of the first module.xxx.php file found
- * in the "collector" directory.
- */
- protected function GetVersionFromModuleFile()
- {
- $aFiles = glob(APPROOT.'collectors/module.*.php');
- if (!$aFiles) {
- // No module found, use a default value...
- $this->sVersion = '1.0.0';
- Utils::Log(LOG_INFO, "Please create a 'module.*.php' file in 'collectors' folder in order to define the version of collectors");
-
- return;
- }
-
- try {
- $sModuleFile = reset($aFiles);
-
- $this->SetProjectNameFromFileName($sModuleFile);
-
- $sModuleFileContents = file_get_contents($sModuleFile);
- $sModuleFileContents = str_replace([''], '', $sModuleFileContents);
- $sModuleFileContents = str_replace('SetupWebPage::AddModule(', '$this->InitFieldsFromModuleContentCallback(', $sModuleFileContents);
- $bRet = eval($sModuleFileContents);
-
- if ($bRet === false) {
- Utils::Log(LOG_WARNING, "Eval of '$sModuleFileContents' returned false");
- }
- } catch (Exception $e) {
- // Continue...
- Utils::Log(LOG_WARNING, "Eval of '$sModuleFileContents' caused an exception: ".$e->getMessage());
- }
- }
-
- /**
- * @param string $sModuleFile
- */
- public function SetProjectNameFromFileName($sModuleFile)
- {
- if (preg_match("/module\.(.*)\.php/", $sModuleFile, $aMatches)) {
- $this->SetProjectName($aMatches[1]);
- }
- }
-
- /**
- * @param string $sProjectName
- */
- public function SetProjectName($sProjectName): void
- {
- $this->sProjectName = $sProjectName;
- Utils::SetProjectName($sProjectName);
- }
-
- /**
- * Sets the $sVersion property. Called when eval'uating the content of the module file
- *
- * @param string $void1 Unused
- * @param string $sId The identifier of the module. Format: 'name/version'
- * @param array $void2 Unused
- */
- protected function InitFieldsFromModuleContentCallback($void1, $sId, $void2)
- {
- if (preg_match('!^(.*)/(.*)$!', $sId, $aMatches)) {
- $this->SetProjectName($aMatches[1]);
- $this->sVersion = $aMatches[2];
- } else {
- $this->sVersion = "1.0.0";
- }
- }
-
- /**
- * Inspects the definition of the Synchro Data Source to find inconsistencies
- *
- * @param mixed[] $aExpectedSourceDefinition
- *
- * @return void
- * @throws Exception
- */
- protected function CheckDataSourceDefinition($aExpectedSourceDefinition)
- {
- Utils::Log(LOG_DEBUG, "Checking the configuration of the data source '{$aExpectedSourceDefinition['name']}'...");
-
- // Check that there is at least 1 reconciliation key, if the reconciliation_policy is "use_attributes"
- if ($aExpectedSourceDefinition['reconciliation_policy'] == 'use_attributes') {
- $bReconciliationKeyFound = false;
- foreach ($aExpectedSourceDefinition['attribute_list'] as $aAttributeDef) {
- if ($aAttributeDef['reconcile'] == '1') {
- $bReconciliationKeyFound = true;
- break;
- }
- }
- if (!$bReconciliationKeyFound) {
- throw new InvalidConfigException("Collector::CheckDataSourceDefinition: Missing reconciliation key for data source '{$aExpectedSourceDefinition['name']}'. ".
- "At least one attribute in 'attribute_list' must have the flag 'reconcile' set to '1'.");
- }
- }
-
- // Check the database table name for invalid characters
- $sDatabaseTableName = $aExpectedSourceDefinition['database_table_name'];
- if (!preg_match(self::TABLENAME_PATTERN, $sDatabaseTableName)) {
- throw new InvalidConfigException("Collector::CheckDataSourceDefinition: '{$aExpectedSourceDefinition['name']}' invalid characters in database_table_name, ".
- "current value is '$sDatabaseTableName'");
- }
-
- Utils::Log(LOG_DEBUG, "The configuration of the data source '{$aExpectedSourceDefinition['name']}' looks correct.");
- }
-
- public function InitSynchroDataSource($aPlaceholders)
- {
- $bResult = true;
- $sJSONSourceDefinition = $this->GetSynchroDataSourceDefinition($aPlaceholders);
- $aExpectedSourceDefinition = json_decode($sJSONSourceDefinition, true);
- $this->CheckDataSourceDefinition($aExpectedSourceDefinition);
-
- $this->sSourceName = $aExpectedSourceDefinition['name'];
- try {
- $oRestClient = new RestClient();
- $aResult = $oRestClient->Get('SynchroDataSource', ['name' => $this->sSourceName]);
- if ($aResult['code'] != 0) {
- Utils::Log(LOG_ERR, "{$aResult['message']} ({$aResult['code']})");
- $bResult = false;
- } else {
- $iCount = ($aResult['objects'] !== null) ? count($aResult['objects']) : 0;
- switch ($iCount) {
- case 0:
- // not found, need to create the Source
- Utils::Log(LOG_INFO, "There is no Synchro Data Source named '{$this->sSourceName}' in iTop. Let's create it.");
- $key = $this->CreateSynchroDataSource($aExpectedSourceDefinition, $this->GetName());
- if ($key === false) {
- $bResult = false;
- }
- break;
-
- case 1:
- foreach ($aResult['objects'] as $sKey => $aData) {
- // Ok, found, is it up to date ?
- $aData = reset($aResult['objects']);
- $aCurrentSourceDefinition = $aData['fields'];
- $iKey = 0;
- if (!array_key_exists('key', $aData)) {
- // Emulate the behavior for older versions of the API
- if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
- $iKey = (int)$aMatches[1];
- }
- } else {
- $iKey = (int)$aData['key'];
- }
- $this->iSourceId = $iKey;
- RestClient::GetFullSynchroDataSource($aCurrentSourceDefinition, $this->iSourceId);
- if ($this->DataSourcesAreEquivalent($aExpectedSourceDefinition, $aCurrentSourceDefinition)) {
- Utils::Log(LOG_INFO, "Ok, the Synchro Data Source '{$this->sSourceName}' exists in iTop and is up to date");
- } else {
- Utils::Log(LOG_INFO, "The Synchro Data Source definition for '{$this->sSourceName}' must be updated in iTop.");
- if (LOG_DEBUG <= Utils::$iConsoleLogLevel) {
- file_put_contents(APPROOT.'data/tmp-'.get_class($this).'-orig.txt', print_r($aExpectedSourceDefinition, true));
- file_put_contents(APPROOT.'data/tmp-'.get_class($this).'-itop.txt', print_r($aCurrentSourceDefinition, true));
- }
- $bResult = $this->UpdateSynchroDataSource($aExpectedSourceDefinition, $this->GetName());
- }
- }
- break;
-
- default:
- // Ambiguous !!
- Utils::Log(LOG_ERR, "There are ".count($aResult['objects'])." Synchro Data Sources named '{$this->sSourceName}' in iTop. Cannot continue.");
- $bResult = false;
- }
- }
- } catch (Exception $e) {
- Utils::Log(LOG_ERR, $e->getMessage());
- $bResult = false;
- }
-
- return $bResult;
- }
-
- public function Collect($iMaxChunkSize = 0)
- {
- Utils::Log(LOG_INFO, get_class($this)." beginning of data collection...");
- try {
- $bResult = $this->Prepare();
- if ($bResult) {
- $idx = 0;
- $aHeaders = null;
- while ($aRow = $this->Fetch()) {
- if ($aHeaders == null) {
- // Check that the row names are consistent with the definition of the task
- $aHeaders = array_keys($aRow);
- }
-
- if (($idx == 0) || (($iMaxChunkSize > 0) && (($idx % $iMaxChunkSize) == 0))) {
- $this->NextCSVFile();
- $this->AddHeader($aHeaders);
- }
-
- $this->AddRow($aRow);
-
- $idx++;
- }
- $this->Cleanup();
- Utils::Log(LOG_INFO, get_class($this)." end of data collection.");
- } else {
- Utils::Log(LOG_ERR, get_class($this)."::Prepare() returned false");
- }
- } catch (Exception $e) {
- $bResult = false;
- Utils::Log(LOG_ERR, get_class($this)."::Collect() got an exception: ".$e->getMessage());
- }
-
- return $bResult;
- }
-
- protected function AddHeader($aHeaders)
- {
- $this->aCSVHeaders = [];
- foreach ($aHeaders as $sHeader) {
- if (($sHeader != 'primary_key') && !$this->HeaderIsAllowed($sHeader)) {
- if (!$this->AttributeIsOptional($sHeader)) {
- Utils::Log(LOG_WARNING, "Invalid column '$sHeader', will be ignored.");
- }
- } else {
- $this->aCSVHeaders[] = $sHeader;
- }
- }
- fputcsv($this->aCSVFile[$this->iFileIndex], $this->aCSVHeaders, $this->sSeparator);
- }
-
- /**
- * Added to add multi-column field support or situations
- * where column name is different than attribute code.
- *
- * @param string $sHeader
- * @return bool
- */
- protected function HeaderIsAllowed($sHeader)
- {
- foreach ($this->aFields as $aField) {
- if (in_array($sHeader, $aField['columns'])) {
- return true;
- }
- }
-
- // fallback old behaviour
- return array_key_exists($sHeader, $this->aFields);
- }
-
- protected function AddRow($aRow)
- {
- $aData = [];
- foreach ($this->aCSVHeaders as $sHeader) {
- if (is_null($aRow[$sHeader]) && $this->AttributeIsNullified($sHeader)) {
- $aData[] = NULL_VALUE;
- } else {
- $aData[] = $aRow[$sHeader];
- }
- }
- //fwrite($this->aCSVFile[$this->iFileIndex], implode($this->sSeparator, $aData)."\n");
- fputcsv($this->aCSVFile[$this->iFileIndex], $aData, $this->sSeparator);
- }
-
- /**
- * @return true
- * @throws IOException When file cannot be opened.
- */
- protected function OpenCSVFile()
- {
- if ($this->MustProcessBeforeSynchro()) {
- $sDataFile = Utils::GetDataFilePath(get_class($this).'.raw-'.(1 + $this->iFileIndex).'.csv');
- } else {
- $sDataFile = Utils::GetDataFilePath(get_class($this).'-'.(1 + $this->iFileIndex).'.csv');
- }
- $this->aCSVFile[$this->iFileIndex] = @fopen($sDataFile, 'wb');
-
- if ($this->aCSVFile[$this->iFileIndex] === false) {
- throw new IOException("Unable to open the file '$sDataFile' for writing.");
- } else {
- Utils::Log(LOG_INFO, "Writing to file '$sDataFile'.");
- }
-
- return true;
- }
-
- /**
- * @return true
- * @throws IOException When file cannot be opened.
- */
- protected function NextCSVFile()
- {
- if ($this->iFileIndex !== null) {
- fclose($this->aCSVFile[$this->iFileIndex]);
- $this->aCSVFile[$this->iFileIndex] = false;
- $this->iFileIndex++;
- } else {
- $this->iFileIndex = 0;
- }
-
- return $this->OpenCSVFile();
- }
-
- protected function RemoveDataFiles()
- {
- $aFiles = glob(Utils::GetDataFilePath(get_class($this).'-*.csv'));
- foreach ($aFiles as $sFile) {
- $bResult = @unlink($sFile);
- Utils::Log(LOG_DEBUG, "Erasing previous data file. unlink('$sFile') returned ".($bResult ? 'true' : 'false'));
- }
- $aFiles = glob(Utils::GetDataFilePath(get_class($this).'.raw-*.csv'));
- foreach ($aFiles as $sFile) {
- $bResult = @unlink($sFile);
- Utils::Log(LOG_DEBUG, "Erasing previous data file. unlink('$sFile') returned ".($bResult ? 'true' : 'false'));
- }
- }
-
- public function Synchronize($iMaxChunkSize = 0)
- {
- // Let a chance to the collector to alter/reprocess the CSV file
- // before running the synchro. This is useful for performing advanced lookups
- // in iTop, for example for some Typology with several cascading levels that
- // the data synchronization can not handle directly
- if ($this->MustProcessBeforeSynchro()) {
- $this->DoProcessBeforeSynchro();
- }
-
- $aFiles = glob(Utils::GetDataFilePath(get_class($this).'-*.csv'));
-
- //use synchro detailed output for log level 7
- //explanation: there is a weard behaviour with LOG level under windows (some PHP versions??)
- // under linux LOG_DEBUG=7 and LOG_INFO=6
- // under windows LOG_DEBUG=LOG_INFO=6...
- $bDetailedOutput = ("7" === "" . Utils::$iConsoleLogLevel);
- foreach ($aFiles as $sDataFile) {
- Utils::Log(LOG_INFO, "Uploading data file '$sDataFile'");
- // Load by chunk
-
- if ($bDetailedOutput) {
- $sOutput = 'details';
- } else {
- $sOutput = 'retcode';
- }
-
- $aData = [
- 'separator' => ';',
- 'data_source_id' => $this->iSourceId,
- 'synchronize' => '0',
- 'no_stop_on_import_error' => 1,
- 'output' => $sOutput,
- 'csvdata' => file_get_contents($sDataFile),
- 'charset' => $this->GetCharset(),
- 'date_format' => Utils::GetConfigurationValue('date_format', 'Y-m-d H:i:s')
- ];
-
- $sLoginform = Utils::GetLoginMode();
- $sResult = self::CallItopViaHttp(
- "/synchro/synchro_import.php?login_mode=$sLoginform",
- $aData
- );
-
- $sTrimmedOutput = trim(strip_tags($sResult));
- $sErrorCount = self::ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput);
-
- if ($sErrorCount != '0') {
- // hmm something went wrong
- Utils::Log(LOG_ERR, "Failed to import the data from '$sDataFile' into iTop. $sErrorCount line(s) had errors.");
- Utils::Log(LOG_ERR, $sTrimmedOutput);
-
- return false;
- } else {
- Utils::Log(LOG_DEBUG, $sTrimmedOutput);
- }
- }
-
- // Synchronize... also by chunks...
- Utils::Log(LOG_INFO, "Starting synchronization of the data source '{$this->sSourceName}'...");
- $aData = [
- 'data_sources' => $this->iSourceId,
- ];
- if ($iMaxChunkSize > 0) {
- $aData['max_chunk_size'] = $iMaxChunkSize;
- }
-
- $sLoginform = Utils::GetLoginMode();
- $sResult = self::CallItopViaHttp(
- "/synchro/synchro_exec.php?login_mode=$sLoginform",
- $aData
- );
-
- $iErrorsCount = $this->ParseSynchroExecOutput($sResult);
-
- return ($iErrorsCount == 0);
- }
-
- /**
- * Detects synchro exec errors and finds out details message
- * @param string $sResult synchro_exec.php output
- * @return int error count
- * @throws Exception
- * @since 1.3.1 N°6771
- */
- public function ParseSynchroExecOutput($sResult): int
- {
- if (preg_match_all('|sErrorMessage .= "Failed to login to iTop. Invalid (or insufficent) credentials.\n";
- return 1;
- }
-
- $iErrorsCount = 0;
- if (preg_match_all('/Objects (.*) errors: ([0-9]+)/', $sResult, $aMatches)) {
- foreach ($aMatches[2] as $sDetailedMessage => $sErrCount) {
- $iErrorsCount += (int)$sErrCount;
- if ((int)$sErrCount > 0) {
- Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' answered: {$aMatches[0][$sDetailedMessage]}");
- $this->sErrorMessage .= $aMatches[0][$sDetailedMessage]."\n";
- }
- }
-
- if (preg_match('/
ERROR: (.*)\./', $sResult, $aMatches)) {
- $sDetailedMessage = $aMatches[1];
- Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' answered: $sDetailedMessage");
- $this->sErrorMessage .= $sDetailedMessage."\n";
- $iErrorsCount++;
- }
- } else {
- Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' failed.");
- Utils::Log(LOG_DEBUG, $sResult);
- $this->sErrorMessage .= $sResult;
- return 1;
- }
-
- if ($iErrorsCount == 0) {
- Utils::Log(LOG_INFO, "Synchronization of data source '{$this->sSourceName}' succeeded.");
- }
-
- return $iErrorsCount;
- }
-
- /**
- * Detects synchro import errors and finds out details message
- * @param string $sTrimmedOutput
- * @param bool $bDetailedOutput
- *
- * @return string: error count string. should match "0" when successfull synchro import.
- * @since 1.3.1 N°6771
- */
- public static function ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput): string
- {
- if ($bDetailedOutput) {
- if (preg_match('/#Issues \(before synchro\): (\d+)/', $sTrimmedOutput, $aMatches)) {
- if (sizeof($aMatches) > 0) {
- return $aMatches[1];
- }
- }
- return -1;
- }
-
- // Read the status code from the last line
- $aLines = explode("\n", $sTrimmedOutput);
- return array_pop($aLines);
- }
-
- /**
- * @deprecated 1.3.1 N°6771
- * @see static::ParseSynchroImportOutput
- */
- public static function ParseSynchroOutput($sTrimmedOutput, $bDetailedOutput): string
- {
- // log a deprecation message
- Utils::Log(LOG_INFO, "Called to deprecated method ParseSynchroOutput. Use ParseSynchroImportOutput instead.");
- return static::ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput);
- }
-
- public static function CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut = -1)
- {
- if (null === static::$oCallItopService) {
- static::$oCallItopService = new CallItopService();
- }
- return static::$oCallItopService->CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut);
- }
-
- protected function CreateSynchroDataSource($aSourceDefinition, $sComment)
- {
- $oClient = new RestClient();
- $ret = false;
-
- // Ignore read-only fields
- unset($aSourceDefinition['friendlyname']);
- unset($aSourceDefinition['user_id_friendlyname']);
- unset($aSourceDefinition['user_id_finalclass_recall']);
- unset($aSourceDefinition['notify_contact_id_friendlyname']);
- unset($aSourceDefinition['notify_contact_id_finalclass_recall']);
- unset($aSourceDefinition['notify_contact_id_obsolescence_flag']);
- // SynchroAttributes will be processed one by one, below
- $aSynchroAttr = $aSourceDefinition['attribute_list'];
- unset($aSourceDefinition['attribute_list']);
-
- $aResult = $oClient->Create('SynchroDataSource', $aSourceDefinition, $sComment);
- if ($aResult['code'] == 0) {
- $aCreatedObj = reset($aResult['objects']);
- $aExpectedAttrDef = $aCreatedObj['fields']['attribute_list'];
- $iKey = (int)$aCreatedObj['key'];
- $this->iSourceId = $iKey;
-
- if ($this->UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttr, $sComment)) {
- $ret = $this->iSourceId;
- }
- } else {
- Utils::Log(LOG_ERR, "Failed to create the SynchroDataSource '{$aSourceDefinition['name']}'. Reason: {$aResult['message']} ({$aResult['code']})");
- }
-
- return $ret;
- }
-
- protected function UpdateSynchroDataSource($aSourceDefinition, $sComment)
- {
- $bRet = true;
- $oClient = new RestClient();
-
- // Ignore read-only fields
- unset($aSourceDefinition['friendlyname']);
- unset($aSourceDefinition['user_id_friendlyname']);
- unset($aSourceDefinition['user_id_finalclass_recall']);
- unset($aSourceDefinition['notify_contact_id_friendlyname']);
- unset($aSourceDefinition['notify_contact_id_finalclass_recall']);
- unset($aSourceDefinition['notify_contact_id_obsolescence_flag']);
- // SynchroAttributes will be processed one by one, below
- $aSynchroAttr = $aSourceDefinition['attribute_list'];
- unset($aSourceDefinition['attribute_list']);
-
- $aResult = $oClient->Update('SynchroDataSource', $this->iSourceId, $aSourceDefinition, $sComment);
- $bRet = ($aResult['code'] == 0);
- if ($bRet) {
- $aUpdatedObj = reset($aResult['objects']);
- $aExpectedAttrDef = $aUpdatedObj['fields']['attribute_list'];
- $bRet = $this->UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttr, $sComment);
- } else {
- Utils::Log(LOG_ERR, "Failed to update the SynchroDataSource '{$aSourceDefinition['name']}' ({$this->iSourceId}). Reason: {$aResult['message']} ({$aResult['code']})");
- }
-
- return $bRet;
- }
-
- protected function UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttrDef, $sComment, ?RestClient $oClient = null)
- {
- $bRet = true;
- if ($oClient === null) {
- $oClient = new RestClient();
- }
-
- foreach ($aSynchroAttrDef as $aAttr) {
- $aExpectedAttr = $this->FindAttr($aAttr['attcode'], $aExpectedAttrDef);
-
- if ($aAttr != $aExpectedAttr) {
- if (($aExpectedAttr === false) && $this->AttributeIsOptional($aAttr['attcode'])) {
- Utils::Log(LOG_INFO, "Skipping optional (and not-present) attribute '{$aAttr['attcode']}'.");
- $this->aSkippedAttributes[] = $aAttr['attcode']; // record that this attribute was skipped
-
- } else {
- // Update only the SynchroAttributes which are really different
- // Ignore read-only fields
- unset($aAttr['friendlyname']);
- $sTargetClass = $aAttr['finalclass'];
- unset($aAttr['finalclass']);
- unset($aAttr['import_columns']);
- // Fix booleans
- $aAttr['update'] = ($aAttr['update'] == 1) ? "1" : "0";
- $aAttr['reconcile'] = ($aAttr['reconcile'] == 1) ? "1" : "0";
-
- Utils::Log(LOG_DEBUG, "Updating attribute '{$aAttr['attcode']}'.");
- $aResult = $oClient->Update($sTargetClass, ['sync_source_id' => $this->iSourceId, 'attcode' => $aAttr['attcode']], $aAttr, $sComment);
- $bRet = ($aResult['code'] == 0);
- if (!$bRet) {
- if (preg_match('/Error: No item found with criteria: sync_source_id/', $aResult['message'])) {
- Utils::Log(LOG_ERR, "Failed to update the Synchro Data Source. Inconsistent data model, the attribute '{$aAttr['attcode']}' does not exist in iTop.");
- } else {
- Utils::Log(LOG_ERR, "Failed to update the SynchroAttribute '{$aAttr['attcode']}'. Reason: {$aResult['message']} ({$aResult['code']})");
- }
- break;
- }
- }
- }
- }
-
- return $bRet;
- }
-
- /**
- * Find the definition of the specified attribute in 'attribute_list'
- *
- * @param string $sAttCode
- * @param array $aExpectedAttrDef
- *
- * @return array|boolean
- */
- protected function FindAttr($sAttCode, $aExpectedAttrDef)
- {
- foreach ($aExpectedAttrDef as $aAttr) {
- if ($aAttr['attcode'] == $sAttCode) {
- return $aAttr;
- }
- }
-
- return false;
- }
-
- /**
- * Smart comparison of two data sources definitions, ignoring optional attributes
- *
- * @param array $aDS1
- * @param array $aDS2
- */
- protected function DataSourcesAreEquivalent($aDS1, $aDS2)
- {
- foreach ($aDS1 as $sKey => $value) {
- switch ($sKey) {
- case 'friendlyname':
- case 'user_id_friendlyname':
- case 'user_id_finalclass_recall':
- case 'notify_contact_id_friendlyname':
- case 'notify_contact_id_finalclass_recall':
- case 'notify_contact_id_obsolescence_flag':
- case 'notify_contact_id_archive_flag':
- // Ignore all read-only attributes
- break;
-
- case 'attribute_list':
- foreach ($value as $sKey => $aDef) {
- $sAttCode = $aDef['attcode'];
- $aDef2 = $this->FindAttr($sAttCode, $aDS2['attribute_list']);
- if ($aDef2 === false) {
- if ($this->AttributeIsOptional($sAttCode)) {
- // Ignore missing optional attributes
- Utils::Log(LOG_DEBUG, "Comparison: ignoring the missing, but optional, attribute: '$sAttCode'.");
- $this->aSkippedAttributes[] = $sAttCode;
- continue;
- } else {
- // Missing non-optional attribute
- Utils::Log(LOG_DEBUG, "Comparison: The definition of the non-optional attribute '$sAttCode' is missing. Data sources differ.");
-
- return false;
- }
-
- } else {
- if (($aDef != $aDef2) && (!$this->AttributeIsOptional($sAttCode))) {
- // Definitions are different
- Utils::Log(LOG_DEBUG, "Comparison: The definitions of the attribute '$sAttCode' are different. Data sources differ:\nExpected values:".print_r($aDef, true)."------------\nCurrent values in iTop:".print_r($aDef2, true)."\n");
-
- return false;
- }
- }
- }
-
- // Now check the other way around: are there too many attributes defined?
- foreach ($aDS2['attribute_list'] as $sKey => $aDef) {
- $sAttCode = $aDef['attcode'];
- if (!$this->FindAttr($sAttCode, $aDS1['attribute_list']) && !$this->AttributeIsOptional($sAttCode)) {
- Utils::Log(LOG_NOTICE, "Comparison: Found the extra definition of the non-optional attribute '$sAttCode' in iTop. Data sources differ. Nothing to do. Update json definition if you want to update this field in iTop.");
- }
- }
- break;
-
- default:
- if (!array_key_exists($sKey, $aDS2) || $aDS2[$sKey] != $value) {
- if ($sKey != 'database_table_name') {
- // one meaningful difference is enough
- Utils::Log(LOG_DEBUG, "Comparison: The property '$sKey' is missing or has a different value. Data sources differ.");
-
- return false;
- }
- }
- }
- }
- //Check the other way around
- foreach ($aDS2 as $sKey => $value) {
- switch ($sKey) {
- case 'friendlyname':
- case 'user_id_friendlyname':
- case 'user_id_finalclass_recall':
- case 'notify_contact_id_friendlyname':
- case 'notify_contact_id_finalclass_recall':
- case 'notify_contact_id_obsolescence_flag':
- case 'notify_contact_id_archive_flag':
- // Ignore all read-only attributes
- break;
-
- default:
- if (!array_key_exists($sKey, $aDS1)) {
- Utils::Log(LOG_DEBUG, "Comparison: Found an extra property '$sKey' in iTop. Data sources differ.");
-
- return false;
- }
- }
- }
-
- return true;
- }
-
- /**
- * @param string $sStep
- *
- * @return array
- */
- public function GetErrorStatus($sStep)
- {
- return [
- 'status' => false,
- 'exit_status' => false,
- 'project' => $this->GetProjectName(),
- 'collector' => get_class($this),
- 'message' => '',
- 'step' => $sStep,
- ];
- }
-
- /**
- * Check if the keys of the supplied hash array match the expected fields listed in the data synchro
- *
- * @param array $aSynchroColumns attribute name as key, attribute's value as value (value not used)
- * @param $aColumnsToIgnore : Elements to ignore
- * @param $sSource : Source of the request (Json file, SQL query, csv file...)
- *
- * @throws \Exception
- */
- protected function CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource)
- {
- $sClass = get_class($this);
- $iError = 0;
-
- if (!array_key_exists('primary_key', $aSynchroColumns)) {
- Utils::Log(LOG_ERR, '['.$sClass.'] The mandatory column "primary_key" is missing in the '.$sSource.'.');
- $iError++;
- }
- foreach ($this->aFields as $sCode => $aDefs) {
- // Skip attributes to ignore
- if (in_array($sCode, $aColumnsToIgnore)) {
- continue;
- }
- // Skip optional attributes
- if ($this->AttributeIsOptional($sCode)) {
- continue;
- }
-
- // Check for missing columns
- $aMissingColumns = array_diff($aDefs['columns'], array_keys($aSynchroColumns));
- if (!empty($aMissingColumns) && $aDefs['reconcile']) {
- Utils::Log(LOG_ERR, sprintf('[%s] The field "%s", used for reconciliation, has missing column(s) in the %s.', $sClass, $sCode, $sSource));
- $iError++;
- Utils::Log(LOG_DEBUG, sprintf('[%s] Missing columns: %s', $sClass, implode(', ', $aMissingColumns)));
- } elseif (!empty($aMissingColumns) && $aDefs['update']) {
- if ($this->AttributeIsNullified($sCode)) {
- Utils::Log(LOG_DEBUG, '['.$sClass.'] The field "'.$sCode.'", used for update, has missing column(s) in first row but nullified.');
- continue;
- }
- Utils::Log(LOG_ERR, sprintf('[%s] The field "%s", used for update, has missing column(s) in the %s.', $sClass, $sCode, $sSource));
- $iError++;
- Utils::Log(LOG_DEBUG, sprintf('[%s] Missing columns: %s', $sClass, implode(', ', $aMissingColumns)));
- }
-
- // Check for useless columns
- if (empty($aMissingColumns) && !$aDefs['reconcile'] && !$aDefs['update']) {
- Utils::Log(LOG_WARNING, sprintf('[%s] The field "%s" is used neither for update nor for reconciliation.', $sClass, $sCode));
- }
-
- }
-
- if ($iError > 0) {
- throw new Exception("Missing columns in the ".$sSource.'.');
- }
- }
-
- /*
- * Check if the collector can be launched
- *
- * @param $aOrchestratedCollectors = list of collectors already orchestrated
- *
- * @return bool
- */
- public function CheckToLaunch(array $aOrchestratedCollectors): bool
- {
- return true;
- }
+ /**
+ * @see N°2417
+ * @var string TABLENAME_PATTERN used to validate data synchro table name
+ */
+ public const TABLENAME_PATTERN = '/^[A-Za-z0-9_]*$/';
+
+ protected $sProjectName;
+ protected $sSynchroDataSourceDefinitionFile;
+ protected $sVersion;
+ protected $iSourceId;
+ protected $aFields;
+ protected $aCSVHeaders;
+ protected $aCSVFile;
+ protected $iFileIndex;
+ protected $aCollectorConfig;
+ protected $sErrorMessage;
+ protected $sSeparator;
+ protected $aSkippedAttributes;
+ protected $aNullifiedAttributes;
+ protected $sSourceName;
+
+ /** @var \CallItopService $oCallItopService */
+ protected static $oCallItopService;
+
+ /**
+ * Construction
+ */
+ public function __construct()
+ {
+ $this->sVersion = null;
+ $this->iSourceId = null;
+ $this->aFields = [];
+ $this->aCSVHeaders = [];
+ $this->aCSVFile = [];
+ $this->iFileIndex = null;
+ $this->aCollectorConfig = [];
+ $this->sErrorMessage = '';
+ $this->sSeparator = ';';
+ $this->aSkippedAttributes = [];
+ }
+
+ /**
+ * @since 1.3.0
+ */
+ public static function SetCallItopService(CallItopService $oCurrentCallItopService)
+ {
+ static::$oCallItopService = $oCurrentCallItopService;
+ }
+
+ /**
+ * Initialization
+ *
+ * @return void
+ * @throws \Exception
+ */
+ public function Init(): void
+ {
+ $sJSONSourceDefinition = $this->GetSynchroDataSourceDefinition();
+ if (empty($sJSONSourceDefinition)) {
+ Utils::Log(
+ LOG_ERR,
+ sprintf(
+ "Empty Synchro Data Source definition for the collector '%s' (file to check/create: %s)",
+ $this->GetName(),
+ $this->sSynchroDataSourceDefinitionFile
+ )
+ );
+ throw new Exception('Cannot create Collector (empty JSON definition)');
+ }
+ $aSourceDefinition = json_decode($sJSONSourceDefinition, true);
+
+ if ($aSourceDefinition === null) {
+ Utils::Log(LOG_ERR, "Invalid Synchro Data Source definition for the collector '".$this->GetName()."' (not a JSON string)");
+ throw new Exception('Cannot create Collector (invalid JSON definition)');
+ }
+ foreach ($aSourceDefinition['attribute_list'] as $aAttr) {
+ $aColumns = isset($aAttr['import_columns']) ? explode(',', $aAttr['import_columns']) : [$aAttr['attcode']];
+ $this->aFields[$aAttr['attcode']] = ['class' => $aAttr['finalclass'], 'update' => ($aAttr['update'] != 0), 'reconcile' => ($aAttr['reconcile'] != 0), 'columns' => $aColumns];
+ }
+
+ $this->ReadCollectorConfig();
+ if (array_key_exists('nullified_attributes', $this->aCollectorConfig)) {
+ $this->aNullifiedAttributes = $this->aCollectorConfig['nullified_attributes'];
+ } else {
+ $this->aNullifiedAttributes = Utils::GetConfigurationValue(get_class($this)."_nullified_attributes", null);
+
+ if ($this->aNullifiedAttributes === null) {
+ // Try all lowercase
+ $this->aNullifiedAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_nullified_attributes", []);
+ }
+ }
+ }
+
+ public function ReadCollectorConfig()
+ {
+ $this->aCollectorConfig = Utils::GetConfigurationValue(get_class($this), []);
+ if (empty($this->aCollectorConfig)) {
+ $this->aCollectorConfig = Utils::GetConfigurationValue(strtolower(get_class($this)), []);
+ }
+ Utils::Log(
+ LOG_DEBUG,
+ sprintf(
+ "aCollectorConfig %s: [%s]",
+ get_class($this),
+ json_encode($this->aCollectorConfig)
+ )
+ );
+ }
+
+ public function GetErrorMessage()
+ {
+ return $this->sErrorMessage;
+ }
+
+ public function GetProjectName()
+ {
+ return $this->sProjectName;
+ }
+
+ protected function Fetch()
+ {
+ // Implement your own mechanism, unless you completely overload Collect()
+ }
+
+ protected function Prepare()
+ {
+ $this->RemoveDataFiles();
+ $this->sSeparator = ';';
+
+ return true;
+ }
+
+ protected function Cleanup()
+ {
+ if ($this->iFileIndex !== null) {
+ fclose($this->aCSVFile[$this->iFileIndex]);
+ }
+ }
+
+ /*
+ * Look for the synchro data source definition file in the different possible collector directories
+ *
+ * @return false|string
+ */
+ public function GetSynchroDataSourceDefinitionFile()
+ {
+ if (file_exists(APPROOT.'collectors/extensions/json/'.get_class($this).'.json')) {
+ return APPROOT.'collectors/extensions/json/'.get_class($this).'.json';
+ } elseif (file_exists(APPROOT.'collectors/json/'.get_class($this).'.json')) {
+ return APPROOT.'collectors/json/'.get_class($this).'.json';
+ } elseif (file_exists(APPROOT.'collectors/'.get_class($this).'.json')) {
+ return APPROOT.'collectors/'.get_class($this).'.json';
+ } else {
+ return false;
+ }
+ }
+
+ public function GetSynchroDataSourceDefinition($aPlaceHolders = [])
+ {
+ $this->sSynchroDataSourceDefinitionFile = $this->GetSynchroDataSourceDefinitionFile();
+ if ($this->sSynchroDataSourceDefinitionFile === false) {
+ return false;
+ }
+
+ $aPlaceHolders['$version$'] = $this->GetVersion();
+ $sSynchroDataSourceDefinition = file_get_contents($this->sSynchroDataSourceDefinitionFile);
+ $sSynchroDataSourceDefinition = str_replace(array_keys($aPlaceHolders), array_values($aPlaceHolders), $sSynchroDataSourceDefinition);
+
+ return $sSynchroDataSourceDefinition;
+ }
+
+ /**
+ * Determine if a given attribute can be missing in the data datamodel.
+ *
+ * Overload this method to let your collector adapt to various datamodels. If an attribute is skipped,
+ * its name is recorded in the member variable $this->aSkippedAttributes for later reference.
+ *
+ * @param string $sAttCode
+ *
+ * @return boolean True if the attribute can be skipped, false otherwise
+ */
+ public function AttributeIsOptional($sAttCode)
+ {
+ return false; // By default no attribute is optional
+ }
+
+ /**
+ * Determine if a given attribute null value is allowed to be transformed between collect and data synchro steps.
+ *
+ * If transformed, its null value is replaced by '' and sent to iTop data synchro.
+ * It means that existing value on iTop side will be kept as is.
+ *
+ * Otherwise if not transformed, empty string value will be sent to datasynchro which means resetting current value on iTop side.
+ *
+ * Best practice:
+ * for fields like decimal, integer or enum, we recommend to configure transformation as resetting will fail on iTop side (Bug N°776).
+ *
+ * The implementation is based on a predefined configuration parameter named from the
+ * class of the collector (all lowercase) with _nullified_attributes appended.
+ *
+ * Example: here is the configuration to "nullify" the attribute 'location_id' for the class MyCollector:
+ *
+ * location_id
+ *
+ *
+ * @param string $sAttCode
+ *
+ * @return boolean True if the attribute can be skipped, false otherwise
+ */
+ public function AttributeIsNullified($sAttCode)
+ {
+ if (is_array($this->aNullifiedAttributes)) {
+ return in_array($sAttCode, $this->aNullifiedAttributes);
+ }
+
+ return false;
+ }
+
+ public function GetName()
+ {
+ return get_class($this);
+ }
+
+ public function GetVersion()
+ {
+ if ($this->sVersion == null) {
+ $this->GetVersionFromModuleFile();
+ }
+
+ return $this->sVersion;
+ }
+
+ /**
+ * Overload this method (to return true) if the collector has
+ * to reprocess the CSV file (with an access to iTop)
+ * before executing the synchro with iTop
+ *
+ * @return bool
+ */
+ protected function MustProcessBeforeSynchro()
+ {
+ // Overload this method (to return true) if the collector has
+ // to reprocess the CSV file (with an access to iTop)
+ // before executing the synchro with iTop
+ return false;
+ }
+
+ /**
+ * Overload this method to perform any one-time initialization which
+ * may be required before processing the CSV file line by line
+ *
+ * @return void
+ */
+ protected function InitProcessBeforeSynchro()
+ {
+ }
+
+ /**
+ * Overload this method to process each line of the CSV file
+ * Should you need to "reject" the line from the ouput, throw an exception of class IgnoredRowException
+ * Example:
+ * throw new IgnoredRowException('Explain why the line is rejected - visible in the debug output');
+ *
+ * @param array $aLineData
+ * @param int $iLineIndex
+ *
+ * @return void
+ * @throws IgnoredRowException
+ */
+ protected function ProcessLineBeforeSynchro(&$aLineData, $iLineIndex)
+ {
+ }
+
+ protected function DoProcessBeforeSynchro()
+ {
+ $this->InitProcessBeforeSynchro();
+
+ $aFiles = glob(Utils::GetDataFilePath(get_class($this).'.raw-*.csv'));
+ foreach ($aFiles as $sDataFile) {
+ Utils::Log(LOG_INFO, "Processing '$sDataFile'...");
+ // Warning backslashes inside the file path (like C:\ on Windows) must be escaped (i.e. \ => \\), but inside a PHP string \ is written '\\' so \\ becomes '\\\\' !!
+ $sPattern = '|'.str_replace('\\', '\\\\', Utils::GetDataFilePath(get_class($this))).'\\.raw-([0-9]+)\\.csv$|';
+ if (preg_match($sPattern, $sDataFile, $aMatches)) {
+ $idx = $aMatches[1];
+ $sOutputFile = Utils::GetDataFilePath(get_class($this).'-'.$idx.'.csv');
+ Utils::Log(LOG_DEBUG, "Converting '$sDataFile' to '$sOutputFile'...");
+
+ $hCSV = fopen($sDataFile, 'r');
+ if ($hCSV === false) {
+ Utils::Log(LOG_ERR, "Failed to open '$sDataFile' for reading... file will be skipped.");
+ }
+
+ $hOutputCSV = fopen($sOutputFile, 'w');
+ if ($hOutputCSV === false) {
+ Utils::Log(LOG_ERR, "Failed to open '$sOutputFile' for writing... file will be skipped.");
+ }
+
+ if (($hCSV !== false) && ($hOutputCSV !== false)) {
+ $iLineIndex = 0;
+ while (($aData = fgetcsv($hCSV, 10000, $this->sSeparator)) !== false) {
+ //process
+ try {
+ $this->ProcessLineBeforeSynchro($aData, $iLineIndex);
+ // Write the CSV data
+ fputcsv($hOutputCSV, $aData, $this->sSeparator);
+ } catch (IgnoredRowException $e) {
+ // Skip this line
+ Utils::Log(LOG_DEBUG, "Ignoring the line $iLineIndex. Reason: ".$e->getMessage());
+ }
+ $iLineIndex++;
+ }
+ fclose($hCSV);
+ fclose($hOutputCSV);
+ Utils::Log(LOG_INFO, "End of processing of '$sDataFile'...");
+ }
+ } else {
+ Utils::Log(LOG_DEBUG, "'$sDataFile' does not match '$sPattern'... file will be skipped.");
+ }
+ }
+ }
+
+ /**
+ * Overload this method if the data collected is in a different character set
+ *
+ * @return string The name of the character set in which the collected data are encoded
+ */
+ protected function GetCharset()
+ {
+ return 'UTF-8';
+ }
+
+ /////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Extracts the version number by evaluating the content of the first module.xxx.php file found
+ * in the "collector" directory.
+ */
+ protected function GetVersionFromModuleFile()
+ {
+ $aFiles = glob(APPROOT.'collectors/module.*.php');
+ if (!$aFiles) {
+ // No module found, use a default value...
+ $this->sVersion = '1.0.0';
+ Utils::Log(LOG_INFO, "Please create a 'module.*.php' file in 'collectors' folder in order to define the version of collectors");
+
+ return;
+ }
+
+ try {
+ $sModuleFile = reset($aFiles);
+
+ $this->SetProjectNameFromFileName($sModuleFile);
+
+ $sModuleFileContents = file_get_contents($sModuleFile);
+ $sModuleFileContents = str_replace([''], '', $sModuleFileContents);
+ $sModuleFileContents = str_replace('SetupWebPage::AddModule(', '$this->InitFieldsFromModuleContentCallback(', $sModuleFileContents);
+ $bRet = eval($sModuleFileContents);
+
+ if ($bRet === false) {
+ Utils::Log(LOG_WARNING, "Eval of '$sModuleFileContents' returned false");
+ }
+ } catch (Exception $e) {
+ // Continue...
+ Utils::Log(LOG_WARNING, "Eval of '$sModuleFileContents' caused an exception: ".$e->getMessage());
+ }
+ }
+
+ /**
+ * @param string $sModuleFile
+ */
+ public function SetProjectNameFromFileName($sModuleFile)
+ {
+ if (preg_match("/module\.(.*)\.php/", $sModuleFile, $aMatches)) {
+ $this->SetProjectName($aMatches[1]);
+ }
+ }
+
+ /**
+ * @param string $sProjectName
+ */
+ public function SetProjectName($sProjectName): void
+ {
+ $this->sProjectName = $sProjectName;
+ Utils::SetProjectName($sProjectName);
+ }
+
+ /**
+ * Sets the $sVersion property. Called when eval'uating the content of the module file
+ *
+ * @param string $void1 Unused
+ * @param string $sId The identifier of the module. Format: 'name/version'
+ * @param array $void2 Unused
+ */
+ protected function InitFieldsFromModuleContentCallback($void1, $sId, $void2)
+ {
+ if (preg_match('!^(.*)/(.*)$!', $sId, $aMatches)) {
+ $this->SetProjectName($aMatches[1]);
+ $this->sVersion = $aMatches[2];
+ } else {
+ $this->sVersion = "1.0.0";
+ }
+ }
+
+ /**
+ * Inspects the definition of the Synchro Data Source to find inconsistencies
+ *
+ * @param mixed[] $aExpectedSourceDefinition
+ *
+ * @return void
+ * @throws Exception
+ */
+ protected function CheckDataSourceDefinition($aExpectedSourceDefinition)
+ {
+ Utils::Log(LOG_DEBUG, "Checking the configuration of the data source '{$aExpectedSourceDefinition['name']}'...");
+
+ // Check that there is at least 1 reconciliation key, if the reconciliation_policy is "use_attributes"
+ if ($aExpectedSourceDefinition['reconciliation_policy'] == 'use_attributes') {
+ $bReconciliationKeyFound = false;
+ foreach ($aExpectedSourceDefinition['attribute_list'] as $aAttributeDef) {
+ if ($aAttributeDef['reconcile'] == '1') {
+ $bReconciliationKeyFound = true;
+ break;
+ }
+ }
+ if (!$bReconciliationKeyFound) {
+ throw new InvalidConfigException("Collector::CheckDataSourceDefinition: Missing reconciliation key for data source '{$aExpectedSourceDefinition['name']}'. ".
+ "At least one attribute in 'attribute_list' must have the flag 'reconcile' set to '1'.");
+ }
+ }
+
+ // Check the database table name for invalid characters
+ $sDatabaseTableName = $aExpectedSourceDefinition['database_table_name'];
+ if (!preg_match(self::TABLENAME_PATTERN, $sDatabaseTableName)) {
+ throw new InvalidConfigException("Collector::CheckDataSourceDefinition: '{$aExpectedSourceDefinition['name']}' invalid characters in database_table_name, ".
+ "current value is '$sDatabaseTableName'");
+ }
+
+ Utils::Log(LOG_DEBUG, "The configuration of the data source '{$aExpectedSourceDefinition['name']}' looks correct.");
+ }
+
+ public function InitSynchroDataSource($aPlaceholders)
+ {
+ $bResult = true;
+ $sJSONSourceDefinition = $this->GetSynchroDataSourceDefinition($aPlaceholders);
+ $aExpectedSourceDefinition = json_decode($sJSONSourceDefinition, true);
+ $this->CheckDataSourceDefinition($aExpectedSourceDefinition);
+
+ $this->sSourceName = $aExpectedSourceDefinition['name'];
+ try {
+ $oRestClient = new RestClient();
+ $aResult = $oRestClient->Get('SynchroDataSource', ['name' => $this->sSourceName]);
+ if ($aResult['code'] != 0) {
+ Utils::Log(LOG_ERR, "{$aResult['message']} ({$aResult['code']})");
+ $bResult = false;
+ } else {
+ $iCount = ($aResult['objects'] !== null) ? count($aResult['objects']) : 0;
+ switch ($iCount) {
+ case 0:
+ // not found, need to create the Source
+ Utils::Log(LOG_INFO, "There is no Synchro Data Source named '{$this->sSourceName}' in iTop. Let's create it.");
+ $key = $this->CreateSynchroDataSource($aExpectedSourceDefinition, $this->GetName());
+ if ($key === false) {
+ $bResult = false;
+ }
+ break;
+
+ case 1:
+ foreach ($aResult['objects'] as $sKey => $aData) {
+ // Ok, found, is it up to date ?
+ $aData = reset($aResult['objects']);
+ $aCurrentSourceDefinition = $aData['fields'];
+ $iKey = 0;
+ if (!array_key_exists('key', $aData)) {
+ // Emulate the behavior for older versions of the API
+ if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
+ $iKey = (int)$aMatches[1];
+ }
+ } else {
+ $iKey = (int)$aData['key'];
+ }
+ $this->iSourceId = $iKey;
+ RestClient::GetFullSynchroDataSource($aCurrentSourceDefinition, $this->iSourceId);
+ if ($this->DataSourcesAreEquivalent($aExpectedSourceDefinition, $aCurrentSourceDefinition)) {
+ Utils::Log(LOG_INFO, "Ok, the Synchro Data Source '{$this->sSourceName}' exists in iTop and is up to date");
+ } else {
+ Utils::Log(LOG_INFO, "The Synchro Data Source definition for '{$this->sSourceName}' must be updated in iTop.");
+ if (LOG_DEBUG <= Utils::$iConsoleLogLevel) {
+ file_put_contents(APPROOT.'data/tmp-'.get_class($this).'-orig.txt', print_r($aExpectedSourceDefinition, true));
+ file_put_contents(APPROOT.'data/tmp-'.get_class($this).'-itop.txt', print_r($aCurrentSourceDefinition, true));
+ }
+ $bResult = $this->UpdateSynchroDataSource($aExpectedSourceDefinition, $this->GetName());
+ }
+ }
+ break;
+
+ default:
+ // Ambiguous !!
+ Utils::Log(LOG_ERR, "There are ".count($aResult['objects'])." Synchro Data Sources named '{$this->sSourceName}' in iTop. Cannot continue.");
+ $bResult = false;
+ }
+ }
+ } catch (Exception $e) {
+ Utils::Log(LOG_ERR, $e->getMessage());
+ $bResult = false;
+ }
+
+ return $bResult;
+ }
+
+ public function Collect($iMaxChunkSize = 0)
+ {
+ Utils::Log(LOG_INFO, get_class($this)." beginning of data collection...");
+ try {
+ $bResult = $this->Prepare();
+ if ($bResult) {
+ $idx = 0;
+ $aHeaders = null;
+ while ($aRow = $this->Fetch()) {
+ if ($aHeaders == null) {
+ // Check that the row names are consistent with the definition of the task
+ $aHeaders = array_keys($aRow);
+ }
+
+ if (($idx == 0) || (($iMaxChunkSize > 0) && (($idx % $iMaxChunkSize) == 0))) {
+ $this->NextCSVFile();
+ $this->AddHeader($aHeaders);
+ }
+
+ $this->AddRow($aRow);
+
+ $idx++;
+ }
+ $this->Cleanup();
+ Utils::Log(LOG_INFO, get_class($this)." end of data collection.");
+ } else {
+ Utils::Log(LOG_ERR, get_class($this)."::Prepare() returned false");
+ }
+ } catch (Exception $e) {
+ $bResult = false;
+ Utils::Log(LOG_ERR, get_class($this)."::Collect() got an exception: ".$e->getMessage());
+ }
+
+ return $bResult;
+ }
+
+ protected function AddHeader($aHeaders)
+ {
+ $this->aCSVHeaders = [];
+ foreach ($aHeaders as $sHeader) {
+ if (($sHeader != 'primary_key') && !$this->HeaderIsAllowed($sHeader)) {
+ if (!$this->AttributeIsOptional($sHeader)) {
+ Utils::Log(LOG_WARNING, "Invalid column '$sHeader', will be ignored.");
+ }
+ } else {
+ $this->aCSVHeaders[] = $sHeader;
+ }
+ }
+ fputcsv($this->aCSVFile[$this->iFileIndex], $this->aCSVHeaders, $this->sSeparator);
+ }
+
+ /**
+ * Added to add multi-column field support or situations
+ * where column name is different than attribute code.
+ *
+ * @param string $sHeader
+ * @return bool
+ */
+ protected function HeaderIsAllowed($sHeader)
+ {
+ foreach ($this->aFields as $aField) {
+ if (in_array($sHeader, $aField['columns'])) {
+ return true;
+ }
+ }
+
+ // fallback old behaviour
+ return array_key_exists($sHeader, $this->aFields);
+ }
+
+ protected function AddRow($aRow)
+ {
+ $aData = [];
+ foreach ($this->aCSVHeaders as $sHeader) {
+ if (is_null($aRow[$sHeader]) && $this->AttributeIsNullified($sHeader)) {
+ $aData[] = NULL_VALUE;
+ } else {
+ $aData[] = $aRow[$sHeader];
+ }
+ }
+ //fwrite($this->aCSVFile[$this->iFileIndex], implode($this->sSeparator, $aData)."\n");
+ fputcsv($this->aCSVFile[$this->iFileIndex], $aData, $this->sSeparator);
+ }
+
+ /**
+ * @return true
+ * @throws IOException When file cannot be opened.
+ */
+ protected function OpenCSVFile()
+ {
+ if ($this->MustProcessBeforeSynchro()) {
+ $sDataFile = Utils::GetDataFilePath(get_class($this).'.raw-'.(1 + $this->iFileIndex).'.csv');
+ } else {
+ $sDataFile = Utils::GetDataFilePath(get_class($this).'-'.(1 + $this->iFileIndex).'.csv');
+ }
+ $this->aCSVFile[$this->iFileIndex] = @fopen($sDataFile, 'wb');
+
+ if ($this->aCSVFile[$this->iFileIndex] === false) {
+ throw new IOException("Unable to open the file '$sDataFile' for writing.");
+ } else {
+ Utils::Log(LOG_INFO, "Writing to file '$sDataFile'.");
+ }
+
+ return true;
+ }
+
+ /**
+ * @return true
+ * @throws IOException When file cannot be opened.
+ */
+ protected function NextCSVFile()
+ {
+ if ($this->iFileIndex !== null) {
+ fclose($this->aCSVFile[$this->iFileIndex]);
+ $this->aCSVFile[$this->iFileIndex] = false;
+ $this->iFileIndex++;
+ } else {
+ $this->iFileIndex = 0;
+ }
+
+ return $this->OpenCSVFile();
+ }
+
+ protected function RemoveDataFiles()
+ {
+ $aFiles = glob(Utils::GetDataFilePath(get_class($this).'-*.csv'));
+ foreach ($aFiles as $sFile) {
+ $bResult = @unlink($sFile);
+ Utils::Log(LOG_DEBUG, "Erasing previous data file. unlink('$sFile') returned ".($bResult ? 'true' : 'false'));
+ }
+ $aFiles = glob(Utils::GetDataFilePath(get_class($this).'.raw-*.csv'));
+ foreach ($aFiles as $sFile) {
+ $bResult = @unlink($sFile);
+ Utils::Log(LOG_DEBUG, "Erasing previous data file. unlink('$sFile') returned ".($bResult ? 'true' : 'false'));
+ }
+ }
+
+ public function Synchronize($iMaxChunkSize = 0)
+ {
+ // Let a chance to the collector to alter/reprocess the CSV file
+ // before running the synchro. This is useful for performing advanced lookups
+ // in iTop, for example for some Typology with several cascading levels that
+ // the data synchronization can not handle directly
+ if ($this->MustProcessBeforeSynchro()) {
+ $this->DoProcessBeforeSynchro();
+ }
+
+ $aFiles = glob(Utils::GetDataFilePath(get_class($this).'-*.csv'));
+
+ //use synchro detailed output for log level 7
+ //explanation: there is a weard behaviour with LOG level under windows (some PHP versions??)
+ // under linux LOG_DEBUG=7 and LOG_INFO=6
+ // under windows LOG_DEBUG=LOG_INFO=6...
+ $bDetailedOutput = ("7" === "" . Utils::$iConsoleLogLevel);
+ foreach ($aFiles as $sDataFile) {
+ Utils::Log(LOG_INFO, "Uploading data file '$sDataFile'");
+ // Load by chunk
+
+ if ($bDetailedOutput) {
+ $sOutput = 'details';
+ } else {
+ $sOutput = 'retcode';
+ }
+
+ $aData = [
+ 'separator' => ';',
+ 'data_source_id' => $this->iSourceId,
+ 'synchronize' => '0',
+ 'no_stop_on_import_error' => 1,
+ 'output' => $sOutput,
+ 'csvdata' => file_get_contents($sDataFile),
+ 'charset' => $this->GetCharset(),
+ 'date_format' => Utils::GetConfigurationValue('date_format', 'Y-m-d H:i:s')
+ ];
+
+ $sLoginform = Utils::GetLoginMode();
+ $sResult = self::CallItopViaHttp(
+ "/synchro/synchro_import.php?login_mode=$sLoginform",
+ $aData
+ );
+
+ $sTrimmedOutput = trim(strip_tags($sResult));
+ $sErrorCount = self::ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput);
+
+ if ($sErrorCount != '0') {
+ // hmm something went wrong
+ Utils::Log(LOG_ERR, "Failed to import the data from '$sDataFile' into iTop. $sErrorCount line(s) had errors.");
+ Utils::Log(LOG_ERR, $sTrimmedOutput);
+
+ return false;
+ } else {
+ Utils::Log(LOG_DEBUG, $sTrimmedOutput);
+ }
+ }
+
+ // Synchronize... also by chunks...
+ Utils::Log(LOG_INFO, "Starting synchronization of the data source '{$this->sSourceName}'...");
+ $aData = [
+ 'data_sources' => $this->iSourceId,
+ ];
+ if ($iMaxChunkSize > 0) {
+ $aData['max_chunk_size'] = $iMaxChunkSize;
+ }
+
+ $sLoginform = Utils::GetLoginMode();
+ $sResult = self::CallItopViaHttp(
+ "/synchro/synchro_exec.php?login_mode=$sLoginform",
+ $aData
+ );
+
+ $iErrorsCount = $this->ParseSynchroExecOutput($sResult);
+
+ return ($iErrorsCount == 0);
+ }
+
+ /**
+ * Detects synchro exec errors and finds out details message
+ * @param string $sResult synchro_exec.php output
+ * @return int error count
+ * @throws Exception
+ * @since 1.3.1 N°6771
+ */
+ public function ParseSynchroExecOutput($sResult): int
+ {
+ if (preg_match_all('|sErrorMessage .= "Failed to login to iTop. Invalid (or insufficent) credentials.\n";
+ return 1;
+ }
+
+ $iErrorsCount = 0;
+ if (preg_match_all('/Objects (.*) errors: ([0-9]+)/', $sResult, $aMatches)) {
+ foreach ($aMatches[2] as $sDetailedMessage => $sErrCount) {
+ $iErrorsCount += (int)$sErrCount;
+ if ((int)$sErrCount > 0) {
+ Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' answered: {$aMatches[0][$sDetailedMessage]}");
+ $this->sErrorMessage .= $aMatches[0][$sDetailedMessage]."\n";
+ }
+ }
+
+ if (preg_match('/
ERROR: (.*)\./', $sResult, $aMatches)) {
+ $sDetailedMessage = $aMatches[1];
+ Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' answered: $sDetailedMessage");
+ $this->sErrorMessage .= $sDetailedMessage."\n";
+ $iErrorsCount++;
+ }
+ } else {
+ Utils::Log(LOG_ERR, "Synchronization of data source '{$this->sSourceName}' failed.");
+ Utils::Log(LOG_DEBUG, $sResult);
+ $this->sErrorMessage .= $sResult;
+ return 1;
+ }
+
+ if ($iErrorsCount == 0) {
+ Utils::Log(LOG_INFO, "Synchronization of data source '{$this->sSourceName}' succeeded.");
+ }
+
+ return $iErrorsCount;
+ }
+
+ /**
+ * Detects synchro import errors and finds out details message
+ * @param string $sTrimmedOutput
+ * @param bool $bDetailedOutput
+ *
+ * @return string: error count string. should match "0" when successfull synchro import.
+ * @since 1.3.1 N°6771
+ */
+ public static function ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput): string
+ {
+ if ($bDetailedOutput) {
+ if (preg_match('/#Issues \(before synchro\): (\d+)/', $sTrimmedOutput, $aMatches)) {
+ if (sizeof($aMatches) > 0) {
+ return $aMatches[1];
+ }
+ }
+ return -1;
+ }
+
+ // Read the status code from the last line
+ $aLines = explode("\n", $sTrimmedOutput);
+ return array_pop($aLines);
+ }
+
+ /**
+ * @deprecated 1.3.1 N°6771
+ * @see static::ParseSynchroImportOutput
+ */
+ public static function ParseSynchroOutput($sTrimmedOutput, $bDetailedOutput): string
+ {
+ // log a deprecation message
+ Utils::Log(LOG_INFO, "Called to deprecated method ParseSynchroOutput. Use ParseSynchroImportOutput instead.");
+ return static::ParseSynchroImportOutput($sTrimmedOutput, $bDetailedOutput);
+ }
+
+ public static function CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut = -1)
+ {
+ if (null === static::$oCallItopService) {
+ static::$oCallItopService = new CallItopService();
+ }
+ return static::$oCallItopService->CallItopViaHttp($sUri, $aAdditionalData, $iTimeOut);
+ }
+
+ protected function CreateSynchroDataSource($aSourceDefinition, $sComment)
+ {
+ $oClient = new RestClient();
+ $ret = false;
+
+ // Ignore read-only fields
+ unset($aSourceDefinition['friendlyname']);
+ unset($aSourceDefinition['user_id_friendlyname']);
+ unset($aSourceDefinition['user_id_finalclass_recall']);
+ unset($aSourceDefinition['notify_contact_id_friendlyname']);
+ unset($aSourceDefinition['notify_contact_id_finalclass_recall']);
+ unset($aSourceDefinition['notify_contact_id_obsolescence_flag']);
+ // SynchroAttributes will be processed one by one, below
+ $aSynchroAttr = $aSourceDefinition['attribute_list'];
+ unset($aSourceDefinition['attribute_list']);
+
+ $aResult = $oClient->Create('SynchroDataSource', $aSourceDefinition, $sComment);
+ if ($aResult['code'] == 0) {
+ $aCreatedObj = reset($aResult['objects']);
+ $aExpectedAttrDef = $aCreatedObj['fields']['attribute_list'];
+ $iKey = (int)$aCreatedObj['key'];
+ $this->iSourceId = $iKey;
+
+ if ($this->UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttr, $sComment)) {
+ $ret = $this->iSourceId;
+ }
+ } else {
+ Utils::Log(LOG_ERR, "Failed to create the SynchroDataSource '{$aSourceDefinition['name']}'. Reason: {$aResult['message']} ({$aResult['code']})");
+ }
+
+ return $ret;
+ }
+
+ protected function UpdateSynchroDataSource($aSourceDefinition, $sComment)
+ {
+ $bRet = true;
+ $oClient = new RestClient();
+
+ // Ignore read-only fields
+ unset($aSourceDefinition['friendlyname']);
+ unset($aSourceDefinition['user_id_friendlyname']);
+ unset($aSourceDefinition['user_id_finalclass_recall']);
+ unset($aSourceDefinition['notify_contact_id_friendlyname']);
+ unset($aSourceDefinition['notify_contact_id_finalclass_recall']);
+ unset($aSourceDefinition['notify_contact_id_obsolescence_flag']);
+ // SynchroAttributes will be processed one by one, below
+ $aSynchroAttr = $aSourceDefinition['attribute_list'];
+ unset($aSourceDefinition['attribute_list']);
+
+ $aResult = $oClient->Update('SynchroDataSource', $this->iSourceId, $aSourceDefinition, $sComment);
+ $bRet = ($aResult['code'] == 0);
+ if ($bRet) {
+ $aUpdatedObj = reset($aResult['objects']);
+ $aExpectedAttrDef = $aUpdatedObj['fields']['attribute_list'];
+ $bRet = $this->UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttr, $sComment);
+ } else {
+ Utils::Log(LOG_ERR, "Failed to update the SynchroDataSource '{$aSourceDefinition['name']}' ({$this->iSourceId}). Reason: {$aResult['message']} ({$aResult['code']})");
+ }
+
+ return $bRet;
+ }
+
+ protected function UpdateSDSAttributes($aExpectedAttrDef, $aSynchroAttrDef, $sComment, ?RestClient $oClient = null)
+ {
+ $bRet = true;
+ if ($oClient === null) {
+ $oClient = new RestClient();
+ }
+
+ foreach ($aSynchroAttrDef as $aAttr) {
+ $aExpectedAttr = $this->FindAttr($aAttr['attcode'], $aExpectedAttrDef);
+
+ if ($aAttr != $aExpectedAttr) {
+ if (($aExpectedAttr === false) && $this->AttributeIsOptional($aAttr['attcode'])) {
+ Utils::Log(LOG_INFO, "Skipping optional (and not-present) attribute '{$aAttr['attcode']}'.");
+ $this->aSkippedAttributes[] = $aAttr['attcode']; // record that this attribute was skipped
+
+ } else {
+ // Update only the SynchroAttributes which are really different
+ // Ignore read-only fields
+ unset($aAttr['friendlyname']);
+ $sTargetClass = $aAttr['finalclass'];
+ unset($aAttr['finalclass']);
+ unset($aAttr['import_columns']);
+ // Fix booleans
+ $aAttr['update'] = ($aAttr['update'] == 1) ? "1" : "0";
+ $aAttr['reconcile'] = ($aAttr['reconcile'] == 1) ? "1" : "0";
+
+ Utils::Log(LOG_DEBUG, "Updating attribute '{$aAttr['attcode']}'.");
+ $aResult = $oClient->Update($sTargetClass, ['sync_source_id' => $this->iSourceId, 'attcode' => $aAttr['attcode']], $aAttr, $sComment);
+ $bRet = ($aResult['code'] == 0);
+ if (!$bRet) {
+ if (preg_match('/Error: No item found with criteria: sync_source_id/', $aResult['message'])) {
+ Utils::Log(LOG_ERR, "Failed to update the Synchro Data Source. Inconsistent data model, the attribute '{$aAttr['attcode']}' does not exist in iTop.");
+ } else {
+ Utils::Log(LOG_ERR, "Failed to update the SynchroAttribute '{$aAttr['attcode']}'. Reason: {$aResult['message']} ({$aResult['code']})");
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ return $bRet;
+ }
+
+ /**
+ * Find the definition of the specified attribute in 'attribute_list'
+ *
+ * @param string $sAttCode
+ * @param array $aExpectedAttrDef
+ *
+ * @return array|boolean
+ */
+ protected function FindAttr($sAttCode, $aExpectedAttrDef)
+ {
+ foreach ($aExpectedAttrDef as $aAttr) {
+ if ($aAttr['attcode'] == $sAttCode) {
+ return $aAttr;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Smart comparison of two data sources definitions, ignoring optional attributes
+ *
+ * @param array $aDS1
+ * @param array $aDS2
+ */
+ protected function DataSourcesAreEquivalent($aDS1, $aDS2)
+ {
+ foreach ($aDS1 as $sKey => $value) {
+ switch ($sKey) {
+ case 'friendlyname':
+ case 'user_id_friendlyname':
+ case 'user_id_finalclass_recall':
+ case 'notify_contact_id_friendlyname':
+ case 'notify_contact_id_finalclass_recall':
+ case 'notify_contact_id_obsolescence_flag':
+ case 'notify_contact_id_archive_flag':
+ // Ignore all read-only attributes
+ break;
+
+ case 'attribute_list':
+ foreach ($value as $sKey => $aDef) {
+ $sAttCode = $aDef['attcode'];
+ $aDef2 = $this->FindAttr($sAttCode, $aDS2['attribute_list']);
+ if ($aDef2 === false) {
+ if ($this->AttributeIsOptional($sAttCode)) {
+ // Ignore missing optional attributes
+ Utils::Log(LOG_DEBUG, "Comparison: ignoring the missing, but optional, attribute: '$sAttCode'.");
+ $this->aSkippedAttributes[] = $sAttCode;
+ continue;
+ } else {
+ // Missing non-optional attribute
+ Utils::Log(LOG_DEBUG, "Comparison: The definition of the non-optional attribute '$sAttCode' is missing. Data sources differ.");
+
+ return false;
+ }
+
+ } else {
+ if (($aDef != $aDef2) && (!$this->AttributeIsOptional($sAttCode))) {
+ // Definitions are different
+ Utils::Log(LOG_DEBUG, "Comparison: The definitions of the attribute '$sAttCode' are different. Data sources differ:\nExpected values:".print_r($aDef, true)."------------\nCurrent values in iTop:".print_r($aDef2, true)."\n");
+
+ return false;
+ }
+ }
+ }
+
+ // Now check the other way around: are there too many attributes defined?
+ foreach ($aDS2['attribute_list'] as $sKey => $aDef) {
+ $sAttCode = $aDef['attcode'];
+ if (!$this->FindAttr($sAttCode, $aDS1['attribute_list']) && !$this->AttributeIsOptional($sAttCode)) {
+ Utils::Log(LOG_NOTICE, "Comparison: Found the extra definition of the non-optional attribute '$sAttCode' in iTop. Data sources differ. Nothing to do. Update json definition if you want to update this field in iTop.");
+ }
+ }
+ break;
+
+ default:
+ if (!array_key_exists($sKey, $aDS2) || $aDS2[$sKey] != $value) {
+ if ($sKey != 'database_table_name') {
+ // one meaningful difference is enough
+ Utils::Log(LOG_DEBUG, "Comparison: The property '$sKey' is missing or has a different value. Data sources differ.");
+
+ return false;
+ }
+ }
+ }
+ }
+ //Check the other way around
+ foreach ($aDS2 as $sKey => $value) {
+ switch ($sKey) {
+ case 'friendlyname':
+ case 'user_id_friendlyname':
+ case 'user_id_finalclass_recall':
+ case 'notify_contact_id_friendlyname':
+ case 'notify_contact_id_finalclass_recall':
+ case 'notify_contact_id_obsolescence_flag':
+ case 'notify_contact_id_archive_flag':
+ // Ignore all read-only attributes
+ break;
+
+ default:
+ if (!array_key_exists($sKey, $aDS1)) {
+ Utils::Log(LOG_DEBUG, "Comparison: Found an extra property '$sKey' in iTop. Data sources differ.");
+
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $sStep
+ *
+ * @return array
+ */
+ public function GetErrorStatus($sStep)
+ {
+ return [
+ 'status' => false,
+ 'exit_status' => false,
+ 'project' => $this->GetProjectName(),
+ 'collector' => get_class($this),
+ 'message' => '',
+ 'step' => $sStep,
+ ];
+ }
+
+ /**
+ * Check if the keys of the supplied hash array match the expected fields listed in the data synchro
+ *
+ * @param array $aSynchroColumns attribute name as key, attribute's value as value (value not used)
+ * @param $aColumnsToIgnore : Elements to ignore
+ * @param $sSource : Source of the request (Json file, SQL query, csv file...)
+ *
+ * @throws \Exception
+ */
+ protected function CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource)
+ {
+ $sClass = get_class($this);
+ $iError = 0;
+
+ if (!array_key_exists('primary_key', $aSynchroColumns)) {
+ Utils::Log(LOG_ERR, '['.$sClass.'] The mandatory column "primary_key" is missing in the '.$sSource.'.');
+ $iError++;
+ }
+ foreach ($this->aFields as $sCode => $aDefs) {
+ // Skip attributes to ignore
+ if (in_array($sCode, $aColumnsToIgnore)) {
+ continue;
+ }
+ // Skip optional attributes
+ if ($this->AttributeIsOptional($sCode)) {
+ continue;
+ }
+
+ // Check for missing columns
+ $aMissingColumns = array_diff($aDefs['columns'], array_keys($aSynchroColumns));
+ if (!empty($aMissingColumns) && $aDefs['reconcile']) {
+ Utils::Log(LOG_ERR, sprintf('[%s] The field "%s", used for reconciliation, has missing column(s) in the %s.', $sClass, $sCode, $sSource));
+ $iError++;
+ Utils::Log(LOG_DEBUG, sprintf('[%s] Missing columns: %s', $sClass, implode(', ', $aMissingColumns)));
+ } elseif (!empty($aMissingColumns) && $aDefs['update']) {
+ if ($this->AttributeIsNullified($sCode)) {
+ Utils::Log(LOG_DEBUG, '['.$sClass.'] The field "'.$sCode.'", used for update, has missing column(s) in first row but nullified.');
+ continue;
+ }
+ Utils::Log(LOG_ERR, sprintf('[%s] The field "%s", used for update, has missing column(s) in the %s.', $sClass, $sCode, $sSource));
+ $iError++;
+ Utils::Log(LOG_DEBUG, sprintf('[%s] Missing columns: %s', $sClass, implode(', ', $aMissingColumns)));
+ }
+
+ // Check for useless columns
+ if (empty($aMissingColumns) && !$aDefs['reconcile'] && !$aDefs['update']) {
+ Utils::Log(LOG_WARNING, sprintf('[%s] The field "%s" is used neither for update nor for reconciliation.', $sClass, $sCode));
+ }
+
+ }
+
+ if ($iError > 0) {
+ throw new Exception("Missing columns in the ".$sSource.'.');
+ }
+ }
+
+ /*
+ * Check if the collector can be launched
+ *
+ * @param $aOrchestratedCollectors = list of collectors already orchestrated
+ *
+ * @return bool
+ */
+ public function CheckToLaunch(array $aOrchestratedCollectors): bool
+ {
+ return true;
+ }
}
diff --git a/core/csvcollector.class.inc.php b/core/csvcollector.class.inc.php
index b9ab208..dbe4c01 100644
--- a/core/csvcollector.class.inc.php
+++ b/core/csvcollector.class.inc.php
@@ -26,366 +26,366 @@
*/
abstract class CSVCollector extends Collector
{
- protected $iIdx = 0;
- protected $aCsvFieldsPerLine = [];
- protected $sCsvSeparator;
- protected $sCsvEncoding;
- protected $bHasHeader = true;
- protected $sCsvCliCommand;
- /**
- * @var array Table of number of columns in input csv file corresponding to output fields or input columns names (if the specification never mention the input column)
- * [0 => [synchro_field1, synchro_field2], 1 => [synchro_field3], 2 => [col_name]]
- */
- protected $aMappingCsvColumnIndexToFields;
- protected $aSynchroFieldsToDefaultValues = [];
- protected $aConfiguredHeaderColumns;
- /**
- * @var array Mapping of csv columns to synchro fields
- * [column_name => [synchro_field1, synchro_field2], column_name2 => [synchro_field3]]
- */
- protected $aMappingCsvColumnNameToFields = [];
- /**
- * @var array Table of all synchronised fields
- * [synchro_field => '', synchro_field2 => '']
- */
- protected $aMappedFields = [];
- protected $aIgnoredCsvColumns = [];
- protected $aIgnoredSynchroFields = [];
-
- /**
- * Initalization
- *
- * @throws Exception
- */
- public function __construct()
- {
- parent::__construct();
- }
-
- /**
- * Parses configured csv file to fetch data
- *
- * @see Collector::Prepare()
- * @throws Exception
- */
- public function Prepare()
- {
- $bRet = parent::Prepare();
-
- $this->sCsvSeparator = ';';
- $this->sCsvEncoding = 'UTF-8';
- $this->sCsvCliCommand = '';
- $this->aSynchroFieldsToDefaultValues = [];
- $this->bHasHeader = true;
-
- $sCsvFilePath = '';
- if (is_array($this->aCollectorConfig)) {
- if (array_key_exists('csv_file', $this->aCollectorConfig)) {
- $sCsvFilePath = $this->aCollectorConfig['csv_file'];
- }
-
- if (array_key_exists('separator', $this->aCollectorConfig)) {
- $this->sCsvSeparator = $this->aCollectorConfig['separator'];
- if ($this->sCsvSeparator === 'TAB') {
- $this->sCsvSeparator = "\t";
- }
- }
- if (array_key_exists('encoding', $this->aCollectorConfig)) {
- $this->sCsvEncoding = $this->aCollectorConfig['encoding'];
- }
- if (array_key_exists('command', $this->aCollectorConfig)) {
- $this->sCsvCliCommand = $this->aCollectorConfig['command'];
- }
- if (array_key_exists('has_header', $this->aCollectorConfig)) {
- $this->bHasHeader = ($this->aCollectorConfig['has_header'] !== 'no');
- }
-
- if (array_key_exists('defaults', $this->aCollectorConfig)) {
- if ($this->aCollectorConfig['defaults'] !== '') {
- $this->aSynchroFieldsToDefaultValues = $this->aCollectorConfig['defaults'];
- if (!is_array($this->aSynchroFieldsToDefaultValues)) {
- Utils::Log(
- LOG_ERR,
- "[".get_class($this)."] defaults section configuration is not correct. please see documentation."
- );
-
- return false;
- }
- }
- }
-
- if (array_key_exists('ignored_columns', $this->aCollectorConfig)) {
- if ($this->aCollectorConfig['ignored_columns'] !== '') {
- if (!is_array($this->aCollectorConfig['ignored_columns'])) {
- Utils::Log(
- LOG_ERR,
- "[".get_class($this)."] ignored_columns section configuration is not correct. please see documentation."
- );
-
- return false;
- }
- $this->aIgnoredCsvColumns = array_values($this->aCollectorConfig['ignored_columns']);
- }
- }
-
- if (array_key_exists('fields', $this->aCollectorConfig)) {
- if ($this->aCollectorConfig['fields'] !== '') {
- $aCurrentConfiguredHeaderColumns = $this->aCollectorConfig['fields'];
- if (!is_array($aCurrentConfiguredHeaderColumns)) {
- Utils::Log(
- LOG_ERR,
- "[".get_class($this)."] fields section configuration is not correct. please see documentation."
- );
-
- return false;
- }
- $this->aConfiguredHeaderColumns = [];
- if ($this->bHasHeader) {
- array_multisort($aCurrentConfiguredHeaderColumns);
- $this->aConfiguredHeaderColumns = array_keys($aCurrentConfiguredHeaderColumns);
-
- foreach ($aCurrentConfiguredHeaderColumns as $sSynchroField => $sCsvColumn) {
- $this->aMappingCsvColumnNameToFields[$sCsvColumn][] = $sSynchroField;
- $this->aMappedFields[$sSynchroField] = '';
- }
- } else {
- $this->aConfiguredHeaderColumns = $aCurrentConfiguredHeaderColumns;
- }
- }
- }
- }
-
- if ($sCsvFilePath === '') {
- // No query at all !!
- Utils::Log(
- LOG_ERR,
- "[".get_class($this)."] no CSV file configured! Cannot collect data. The csv was expected to be configured as '".strtolower(get_class($this))."_csv' in the configuration file."
- );
-
- return false;
- }
-
- Utils::Log(LOG_INFO, "[".get_class($this)."] CSV file is [".$sCsvFilePath."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Has csv header [".($this->bHasHeader ? "yes" : "no")."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Separator used is [".$this->sCsvSeparator."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Encoding used is [".$this->sCsvEncoding."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Fields [".var_export($this->aConfiguredHeaderColumns, true)."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Ignored csv fields [".var_export($this->aIgnoredCsvColumns, true)."]");
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Default values [".var_export($this->aSynchroFieldsToDefaultValues, true)."]");
-
- if (!empty($this->sCsvCliCommand)) {
- Utils::Exec($this->sCsvCliCommand);
- }
-
- try {
- $hHandle = fopen($sCsvFilePath, "r");
- } catch (Exception $e) {
- Utils::Log(LOG_INFO, "[".get_class($this)."] Cannot open CSV file $sCsvFilePath");
- $sCsvFilePath = APPROOT.$sCsvFilePath;
- try {
- $hHandle = fopen($sCsvFilePath, "r");
- } catch (Exception $e) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] Cannot open CSV file $sCsvFilePath");
-
- return false;
- }
- }
-
- if (!$hHandle) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] Cannot use CSV file handle for $sCsvFilePath");
-
- return false;
- }
-
- $sTmpFile = tempnam(sys_get_temp_dir(), "decoded_");
- file_put_contents($sTmpFile, iconv($this->sCsvEncoding, $this->GetCharset(), stream_get_contents($hHandle)));
- $oTmpHandle = fopen($sTmpFile, "r");
-
- while (($aData = fgetcsv($oTmpHandle, 0, $this->sCsvSeparator)) !== false) {
- $this->aCsvFieldsPerLine[] = $aData;
- }
-
- fclose($oTmpHandle);
- unlink($sTmpFile);
-
- return $bRet;
- }
-
- /**
- * @return NextLineObject
- */
- public function getNextLine()
- {
- $aValues = $this->aCsvFieldsPerLine[$this->iIdx];
- $sCsvLine = implode($this->sCsvSeparator, $aValues);
-
- return new NextLineObject($sCsvLine, $aValues);
- }
-
- /**
- * Fetches one csv row at a time
- * The first row is used to check if the columns of the result match the expected "fields"
- *
- * @see Collector::Fetch()
- * @throws Exception
- */
- public function Fetch()
- {
- $iCount = count($this->aCsvFieldsPerLine);
- if (($iCount == 0) || (($iCount == 1) && $this->bHasHeader)) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] CSV file is empty. Data collection stops here.");
-
- return false;
- }
- if ($this->iIdx >= $iCount) {
-
- return false;
- }
-
- /** NextLineObject**/
- $oNextLineArr = $this->getNextLine();
-
- if (!$this->aMappingCsvColumnIndexToFields) {
- $aCsvHeaderColumns = $oNextLineArr->getValues();
-
- $this->Configure($aCsvHeaderColumns);
- $this->CheckColumns($this->aMappedFields, [], 'csv file');
-
- if ($this->bHasHeader) {
- $this->iIdx++;
- /** NextLineObject**/
- $oNextLineArr = $this->getNextLine();
- }
- }
-
- $iColumnSize = count($this->aMappingCsvColumnIndexToFields);
- $iLineSize = count($oNextLineArr->getValues());
- if ($iColumnSize !== $iLineSize) {
- $line = $this->iIdx + 1;
- Utils::Log(
- LOG_ERR,
- "[".get_class($this)."] Wrong number of columns ($iLineSize) on line $line (expected $iColumnSize columns just like in header): ".$oNextLineArr->getCsvLine()
- );
- throw new Exception("Invalid CSV file.");
- }
-
- $aData = [];
-
- foreach ($oNextLineArr->getValues() as $i => $sVal) {
- $aSynchroFields = $this->aMappingCsvColumnIndexToFields[$i];
- foreach ($aSynchroFields as $sSynchroField) {
- if (array_key_exists($sSynchroField, $this->aSynchroFieldsToDefaultValues)) {
- if (empty($sVal)) {
- $aData[$sSynchroField] = $this->aSynchroFieldsToDefaultValues[$sSynchroField];
- } else {
- $aData[$sSynchroField] = $sVal;
- }
- } else {
- if (!in_array($sSynchroField, $this->aIgnoredSynchroFields)) {
- $aData[$sSynchroField] = $sVal;
- }
- }
- }
-
- }
-
- foreach ($this->aSynchroFieldsToDefaultValues as $sAttributeId => $sAttributeValue) {
- if (!array_key_exists($sAttributeId, $aData)) {
- $aData[$sAttributeId] = $sAttributeValue;
- }
- }
-
- $this->iIdx++;
-
- return $aData;
- }
-
- /**
- * @param $aCsvHeaderColumns
- */
- protected function Configure($aCsvHeaderColumns)
- {
- if ($this->bHasHeader) {
- $this->aMappingCsvColumnIndexToFields = [];
-
- foreach ($aCsvHeaderColumns as $sCsvColumn) {
- if (array_key_exists($sCsvColumn, $this->aMappingCsvColumnNameToFields)) {
- //use mapping instead of csv header sSynchroColumn
- $this->aMappingCsvColumnIndexToFields[] = $this->aMappingCsvColumnNameToFields[$sCsvColumn];
- } else {
- if (!array_key_exists($sCsvColumn, $this->aMappedFields)) {
- $this->aMappingCsvColumnIndexToFields[] = [$sCsvColumn];
- $this->aMappingCsvColumnNameToFields[$sCsvColumn] = [$sCsvColumn];
- $this->aMappedFields[$sCsvColumn] = '';
- } else {
- $this->aMappingCsvColumnIndexToFields[] = [''];
- $this->aMappingCsvColumnNameToFields[$sCsvColumn] = [''];
- }
- }
- }
- } else {
- foreach ($this->aConfiguredHeaderColumns as $sSynchroField => $sCsvColumn) {
- $this->aMappingCsvColumnIndexToFields[$sCsvColumn - 1][] = $sSynchroField;
- $this->aMappedFields[$sSynchroField] = '';
- }
- foreach ($this->aIgnoredCsvColumns as $sCsvColumn) {
- $this->aMappingCsvColumnIndexToFields[$sCsvColumn - 1] = ['ignored_attribute_'.$sCsvColumn];
- }
- }
- foreach ($this->aIgnoredCsvColumns as $sIgnoredCsvColumn) {
- $this->aIgnoredSynchroFields = array_merge($this->aIgnoredSynchroFields, ($this->bHasHeader) ? $this->aMappingCsvColumnNameToFields[$sIgnoredCsvColumn] : $this->aMappingCsvColumnIndexToFields[$sIgnoredCsvColumn - 1]);
- }
-
- }
-
- /**
- * @inheritdoc
- */
- protected function CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource)
- {
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Columns [".var_export($aSynchroColumns, true)."]");
- foreach ($this->aFields as $sField => $aDefs) {
- foreach ($aDefs['columns'] as $sSynchroColumn) {
- if (array_key_exists($sSynchroColumn, $this->aSynchroFieldsToDefaultValues) || in_array($sSynchroColumn, $this->aIgnoredSynchroFields)) {
- $aColumnsToIgnore[] = $sField;
- }
- }
- }
-
- parent::CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource);
- }
+ protected $iIdx = 0;
+ protected $aCsvFieldsPerLine = [];
+ protected $sCsvSeparator;
+ protected $sCsvEncoding;
+ protected $bHasHeader = true;
+ protected $sCsvCliCommand;
+ /**
+ * @var array Table of number of columns in input csv file corresponding to output fields or input columns names (if the specification never mention the input column)
+ * [0 => [synchro_field1, synchro_field2], 1 => [synchro_field3], 2 => [col_name]]
+ */
+ protected $aMappingCsvColumnIndexToFields;
+ protected $aSynchroFieldsToDefaultValues = [];
+ protected $aConfiguredHeaderColumns;
+ /**
+ * @var array Mapping of csv columns to synchro fields
+ * [column_name => [synchro_field1, synchro_field2], column_name2 => [synchro_field3]]
+ */
+ protected $aMappingCsvColumnNameToFields = [];
+ /**
+ * @var array Table of all synchronised fields
+ * [synchro_field => '', synchro_field2 => '']
+ */
+ protected $aMappedFields = [];
+ protected $aIgnoredCsvColumns = [];
+ protected $aIgnoredSynchroFields = [];
+
+ /**
+ * Initalization
+ *
+ * @throws Exception
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Parses configured csv file to fetch data
+ *
+ * @see Collector::Prepare()
+ * @throws Exception
+ */
+ public function Prepare()
+ {
+ $bRet = parent::Prepare();
+
+ $this->sCsvSeparator = ';';
+ $this->sCsvEncoding = 'UTF-8';
+ $this->sCsvCliCommand = '';
+ $this->aSynchroFieldsToDefaultValues = [];
+ $this->bHasHeader = true;
+
+ $sCsvFilePath = '';
+ if (is_array($this->aCollectorConfig)) {
+ if (array_key_exists('csv_file', $this->aCollectorConfig)) {
+ $sCsvFilePath = $this->aCollectorConfig['csv_file'];
+ }
+
+ if (array_key_exists('separator', $this->aCollectorConfig)) {
+ $this->sCsvSeparator = $this->aCollectorConfig['separator'];
+ if ($this->sCsvSeparator === 'TAB') {
+ $this->sCsvSeparator = "\t";
+ }
+ }
+ if (array_key_exists('encoding', $this->aCollectorConfig)) {
+ $this->sCsvEncoding = $this->aCollectorConfig['encoding'];
+ }
+ if (array_key_exists('command', $this->aCollectorConfig)) {
+ $this->sCsvCliCommand = $this->aCollectorConfig['command'];
+ }
+ if (array_key_exists('has_header', $this->aCollectorConfig)) {
+ $this->bHasHeader = ($this->aCollectorConfig['has_header'] !== 'no');
+ }
+
+ if (array_key_exists('defaults', $this->aCollectorConfig)) {
+ if ($this->aCollectorConfig['defaults'] !== '') {
+ $this->aSynchroFieldsToDefaultValues = $this->aCollectorConfig['defaults'];
+ if (!is_array($this->aSynchroFieldsToDefaultValues)) {
+ Utils::Log(
+ LOG_ERR,
+ "[".get_class($this)."] defaults section configuration is not correct. please see documentation."
+ );
+
+ return false;
+ }
+ }
+ }
+
+ if (array_key_exists('ignored_columns', $this->aCollectorConfig)) {
+ if ($this->aCollectorConfig['ignored_columns'] !== '') {
+ if (!is_array($this->aCollectorConfig['ignored_columns'])) {
+ Utils::Log(
+ LOG_ERR,
+ "[".get_class($this)."] ignored_columns section configuration is not correct. please see documentation."
+ );
+
+ return false;
+ }
+ $this->aIgnoredCsvColumns = array_values($this->aCollectorConfig['ignored_columns']);
+ }
+ }
+
+ if (array_key_exists('fields', $this->aCollectorConfig)) {
+ if ($this->aCollectorConfig['fields'] !== '') {
+ $aCurrentConfiguredHeaderColumns = $this->aCollectorConfig['fields'];
+ if (!is_array($aCurrentConfiguredHeaderColumns)) {
+ Utils::Log(
+ LOG_ERR,
+ "[".get_class($this)."] fields section configuration is not correct. please see documentation."
+ );
+
+ return false;
+ }
+ $this->aConfiguredHeaderColumns = [];
+ if ($this->bHasHeader) {
+ array_multisort($aCurrentConfiguredHeaderColumns);
+ $this->aConfiguredHeaderColumns = array_keys($aCurrentConfiguredHeaderColumns);
+
+ foreach ($aCurrentConfiguredHeaderColumns as $sSynchroField => $sCsvColumn) {
+ $this->aMappingCsvColumnNameToFields[$sCsvColumn][] = $sSynchroField;
+ $this->aMappedFields[$sSynchroField] = '';
+ }
+ } else {
+ $this->aConfiguredHeaderColumns = $aCurrentConfiguredHeaderColumns;
+ }
+ }
+ }
+ }
+
+ if ($sCsvFilePath === '') {
+ // No query at all !!
+ Utils::Log(
+ LOG_ERR,
+ "[".get_class($this)."] no CSV file configured! Cannot collect data. The csv was expected to be configured as '".strtolower(get_class($this))."_csv' in the configuration file."
+ );
+
+ return false;
+ }
+
+ Utils::Log(LOG_INFO, "[".get_class($this)."] CSV file is [".$sCsvFilePath."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Has csv header [".($this->bHasHeader ? "yes" : "no")."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Separator used is [".$this->sCsvSeparator."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Encoding used is [".$this->sCsvEncoding."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Fields [".var_export($this->aConfiguredHeaderColumns, true)."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Ignored csv fields [".var_export($this->aIgnoredCsvColumns, true)."]");
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Default values [".var_export($this->aSynchroFieldsToDefaultValues, true)."]");
+
+ if (!empty($this->sCsvCliCommand)) {
+ Utils::Exec($this->sCsvCliCommand);
+ }
+
+ try {
+ $hHandle = fopen($sCsvFilePath, "r");
+ } catch (Exception $e) {
+ Utils::Log(LOG_INFO, "[".get_class($this)."] Cannot open CSV file $sCsvFilePath");
+ $sCsvFilePath = APPROOT.$sCsvFilePath;
+ try {
+ $hHandle = fopen($sCsvFilePath, "r");
+ } catch (Exception $e) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Cannot open CSV file $sCsvFilePath");
+
+ return false;
+ }
+ }
+
+ if (!$hHandle) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Cannot use CSV file handle for $sCsvFilePath");
+
+ return false;
+ }
+
+ $sTmpFile = tempnam(sys_get_temp_dir(), "decoded_");
+ file_put_contents($sTmpFile, iconv($this->sCsvEncoding, $this->GetCharset(), stream_get_contents($hHandle)));
+ $oTmpHandle = fopen($sTmpFile, "r");
+
+ while (($aData = fgetcsv($oTmpHandle, 0, $this->sCsvSeparator)) !== false) {
+ $this->aCsvFieldsPerLine[] = $aData;
+ }
+
+ fclose($oTmpHandle);
+ unlink($sTmpFile);
+
+ return $bRet;
+ }
+
+ /**
+ * @return NextLineObject
+ */
+ public function getNextLine()
+ {
+ $aValues = $this->aCsvFieldsPerLine[$this->iIdx];
+ $sCsvLine = implode($this->sCsvSeparator, $aValues);
+
+ return new NextLineObject($sCsvLine, $aValues);
+ }
+
+ /**
+ * Fetches one csv row at a time
+ * The first row is used to check if the columns of the result match the expected "fields"
+ *
+ * @see Collector::Fetch()
+ * @throws Exception
+ */
+ public function Fetch()
+ {
+ $iCount = count($this->aCsvFieldsPerLine);
+ if (($iCount == 0) || (($iCount == 1) && $this->bHasHeader)) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] CSV file is empty. Data collection stops here.");
+
+ return false;
+ }
+ if ($this->iIdx >= $iCount) {
+
+ return false;
+ }
+
+ /** NextLineObject**/
+ $oNextLineArr = $this->getNextLine();
+
+ if (!$this->aMappingCsvColumnIndexToFields) {
+ $aCsvHeaderColumns = $oNextLineArr->getValues();
+
+ $this->Configure($aCsvHeaderColumns);
+ $this->CheckColumns($this->aMappedFields, [], 'csv file');
+
+ if ($this->bHasHeader) {
+ $this->iIdx++;
+ /** NextLineObject**/
+ $oNextLineArr = $this->getNextLine();
+ }
+ }
+
+ $iColumnSize = count($this->aMappingCsvColumnIndexToFields);
+ $iLineSize = count($oNextLineArr->getValues());
+ if ($iColumnSize !== $iLineSize) {
+ $line = $this->iIdx + 1;
+ Utils::Log(
+ LOG_ERR,
+ "[".get_class($this)."] Wrong number of columns ($iLineSize) on line $line (expected $iColumnSize columns just like in header): ".$oNextLineArr->getCsvLine()
+ );
+ throw new Exception("Invalid CSV file.");
+ }
+
+ $aData = [];
+
+ foreach ($oNextLineArr->getValues() as $i => $sVal) {
+ $aSynchroFields = $this->aMappingCsvColumnIndexToFields[$i];
+ foreach ($aSynchroFields as $sSynchroField) {
+ if (array_key_exists($sSynchroField, $this->aSynchroFieldsToDefaultValues)) {
+ if (empty($sVal)) {
+ $aData[$sSynchroField] = $this->aSynchroFieldsToDefaultValues[$sSynchroField];
+ } else {
+ $aData[$sSynchroField] = $sVal;
+ }
+ } else {
+ if (!in_array($sSynchroField, $this->aIgnoredSynchroFields)) {
+ $aData[$sSynchroField] = $sVal;
+ }
+ }
+ }
+
+ }
+
+ foreach ($this->aSynchroFieldsToDefaultValues as $sAttributeId => $sAttributeValue) {
+ if (!array_key_exists($sAttributeId, $aData)) {
+ $aData[$sAttributeId] = $sAttributeValue;
+ }
+ }
+
+ $this->iIdx++;
+
+ return $aData;
+ }
+
+ /**
+ * @param $aCsvHeaderColumns
+ */
+ protected function Configure($aCsvHeaderColumns)
+ {
+ if ($this->bHasHeader) {
+ $this->aMappingCsvColumnIndexToFields = [];
+
+ foreach ($aCsvHeaderColumns as $sCsvColumn) {
+ if (array_key_exists($sCsvColumn, $this->aMappingCsvColumnNameToFields)) {
+ //use mapping instead of csv header sSynchroColumn
+ $this->aMappingCsvColumnIndexToFields[] = $this->aMappingCsvColumnNameToFields[$sCsvColumn];
+ } else {
+ if (!array_key_exists($sCsvColumn, $this->aMappedFields)) {
+ $this->aMappingCsvColumnIndexToFields[] = [$sCsvColumn];
+ $this->aMappingCsvColumnNameToFields[$sCsvColumn] = [$sCsvColumn];
+ $this->aMappedFields[$sCsvColumn] = '';
+ } else {
+ $this->aMappingCsvColumnIndexToFields[] = [''];
+ $this->aMappingCsvColumnNameToFields[$sCsvColumn] = [''];
+ }
+ }
+ }
+ } else {
+ foreach ($this->aConfiguredHeaderColumns as $sSynchroField => $sCsvColumn) {
+ $this->aMappingCsvColumnIndexToFields[$sCsvColumn - 1][] = $sSynchroField;
+ $this->aMappedFields[$sSynchroField] = '';
+ }
+ foreach ($this->aIgnoredCsvColumns as $sCsvColumn) {
+ $this->aMappingCsvColumnIndexToFields[$sCsvColumn - 1] = ['ignored_attribute_'.$sCsvColumn];
+ }
+ }
+ foreach ($this->aIgnoredCsvColumns as $sIgnoredCsvColumn) {
+ $this->aIgnoredSynchroFields = array_merge($this->aIgnoredSynchroFields, ($this->bHasHeader) ? $this->aMappingCsvColumnNameToFields[$sIgnoredCsvColumn] : $this->aMappingCsvColumnIndexToFields[$sIgnoredCsvColumn - 1]);
+ }
+
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource)
+ {
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Columns [".var_export($aSynchroColumns, true)."]");
+ foreach ($this->aFields as $sField => $aDefs) {
+ foreach ($aDefs['columns'] as $sSynchroColumn) {
+ if (array_key_exists($sSynchroColumn, $this->aSynchroFieldsToDefaultValues) || in_array($sSynchroColumn, $this->aIgnoredSynchroFields)) {
+ $aColumnsToIgnore[] = $sField;
+ }
+ }
+ }
+
+ parent::CheckColumns($aSynchroColumns, $aColumnsToIgnore, $sSource);
+ }
}
class NextLineObject
{
- private $sCsvLine;
- private $aValues;
-
- /**
- * NextLineObject constructor.
- *
- * @param $csv_line
- * @param $aValues
- */
- public function __construct($sCsvLine, $aValues)
- {
- $this->sCsvLine = $sCsvLine;
- $this->aValues = $aValues;
- }
-
- /**
- * @return mixed
- */
- public function getCsvLine()
- {
- return $this->sCsvLine;
- }
-
- /**
- * @return mixed
- */
- public function getValues()
- {
- return $this->aValues;
- }
+ private $sCsvLine;
+ private $aValues;
+
+ /**
+ * NextLineObject constructor.
+ *
+ * @param $csv_line
+ * @param $aValues
+ */
+ public function __construct($sCsvLine, $aValues)
+ {
+ $this->sCsvLine = $sCsvLine;
+ $this->aValues = $aValues;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getCsvLine()
+ {
+ return $this->sCsvLine;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getValues()
+ {
+ return $this->aValues;
+ }
}
diff --git a/core/dopostrequestservice.class.inc.php b/core/dopostrequestservice.class.inc.php
index 3e37a91..f2d7dfa 100644
--- a/core/dopostrequestservice.class.inc.php
+++ b/core/dopostrequestservice.class.inc.php
@@ -5,8 +5,8 @@
*/
class DoPostRequestService
{
- public function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = [])
- {
- return null;
- }
+ public function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = [])
+ {
+ return null;
+ }
}
diff --git a/core/jsoncollector.class.inc.php b/core/jsoncollector.class.inc.php
index 52c326e..5562317 100644
--- a/core/jsoncollector.class.inc.php
+++ b/core/jsoncollector.class.inc.php
@@ -33,331 +33,331 @@
*/
abstract class JsonCollector extends Collector
{
- protected $sFileJson;
- protected $aJson;
- protected $sURL;
- protected $sFilePath;
- protected $aJsonKey;
- protected $aFieldsKey;
- protected $sJsonCliCommand;
- protected $iIdx;
- protected $aSynchroFieldsToDefaultValues = [];
-
- /**
- * Initalization
- */
- public function __construct()
- {
- parent::__construct();
- $this->sFileJson = null;
- $this->sURL = null;
- $this->aJson = null;
- $this->aFieldsKey = null;
- $this->iIdx = 0;
- }
-
- /**
- * Runs the configured query to start fetching the data from the database
- *
- * @see Collector::Prepare()
- */
- public function Prepare()
- {
- $bRet = parent::Prepare();
- if (!$bRet) {
- return false;
- }
-
- //**** step 1 : get all parameters from config file
- $aParamsSourceJson = $this->aCollectorConfig;
- if (isset($aParamsSourceJson["command"])) {
- $this->sJsonCliCommand = $aParamsSourceJson["command"];
- }
- if (isset($aParamsSourceJson["COMMAND"])) {
- $this->sJsonCliCommand = $aParamsSourceJson["COMMAND"];
- Utils::Log(LOG_INFO, "[".get_class($this)."] CLI command used is [".$this->sJsonCliCommand."]");
- }
-
- // Read the URL or Path from the configuration
- if (isset($aParamsSourceJson["jsonurl"])) {
- $this->sURL = $aParamsSourceJson["jsonurl"];
- }
- if (isset($aParamsSourceJson["JSONURL"])) {
- $this->sURL = $aParamsSourceJson["JSONURL"];
- }
-
- if ($this->sURL == '') {
- if (isset($aParamsSourceJson["jsonfile"])) {
- $this->sFilePath = $aParamsSourceJson["jsonfile"];
- }
- if (isset($aParamsSourceJson["JSONFILE"])) { // Try all lowercase
- $this->sFilePath = $aParamsSourceJson["JSONFILE"];
- }
- Utils::Log(LOG_INFO, "Source file path: ".$this->sFilePath);
- } else {
- Utils::Log(LOG_INFO, "Source URL: ".$this->sURL);
- }
-
- if ($this->sURL == '' && $this->sFilePath == '') {
- // No query at all !!
- Utils::Log(LOG_ERR, "[".get_class($this)."] no json URL or path configured! Cannot collect data. Please configure it as '' or '' in the configuration file.");
-
- return false;
- }
- if (array_key_exists('defaults', $aParamsSourceJson)) {
- if ($aParamsSourceJson['defaults'] !== '') {
- $this->aSynchroFieldsToDefaultValues = $aParamsSourceJson['defaults'];
- if (!is_array($this->aSynchroFieldsToDefaultValues)) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] defaults section configuration is not correct. please see documentation.");
-
- return false;
- }
- }
- }
-
- $aPath = [];
- if (isset($aParamsSourceJson["path"])) {
- $aPath = explode('/', $aParamsSourceJson["path"]);
- }
- if (isset($aParamsSourceJson["PATH"])) {
- $aPath = explode('/', $aParamsSourceJson["PATH"]);
- }
- if (count($aPath) == 0) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] no path to find data in JSON file");
- }
-
- //**** step 2 : get json file
- //execute cmd before get the json
- if (!empty($this->sJsonCliCommand)) {
- Utils::Exec($this->sJsonCliCommand);
- }
-
- //get Json file
- if ($this->sURL != '') {
- Utils::Log(LOG_DEBUG, 'Get params for uploading data file ');
- $aDataGet = [];
- if (isset($aParamsSourceJson["jsonpost"])) {
- $aDataGet = $aParamsSourceJson['jsonpost'];
- } else {
- $aDataGet = [];
- }
- $iSynchroTimeout = (int)Utils::GetConfigurationValue('itop_synchro_timeout', 600); // timeout in seconds, for a synchro to run
- $aCurlOptions = Utils::GetCurlOptions($iSynchroTimeout);
-
- //logs
- Utils::Log(LOG_DEBUG, 'Source aDataGet: '.json_encode($aDataGet));
- $this->sFileJson = Utils::DoPostRequest($this->sURL, $aDataGet, '', $aResponseHeaders, $aCurlOptions);
- Utils::Log(LOG_DEBUG, 'Source sFileJson: '.$this->sFileJson);
- Utils::Log(LOG_INFO, 'Synchro URL (target): '.Utils::GetConfigurationValue('itop_url', []));
- } else {
- $this->sFileJson = @file_get_contents($this->sFilePath);
- if ($this->sFileJson === false) {
- $this->sFilePath = APPROOT.$this->sFilePath;
- $this->sFileJson = @file_get_contents($this->sFilePath);
- }
- Utils::Log(LOG_DEBUG, 'Source sFileJson: '.$this->sFileJson);
- Utils::Log(LOG_INFO, 'Synchro URL (target): '.Utils::GetConfigurationValue('itop_url', []));
- }
-
- //verify the file
- if ($this->sFileJson === false) {
- Utils::Log(LOG_ERR, '['.get_class($this).'] Failed to get JSON file: '.$this->sURL);
-
- return false;
- }
-
- //**** step 3 : read json file
- $this->aJson = json_decode($this->sFileJson, true);
- if ($this->aJson == null) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to translate data from JSON file: '".$this->sURL.$this->sFilePath."'. Reason: ".json_last_error_msg());
-
- return false;
- }
-
- //Get table of Element in JSON file with a specific path
- foreach ($aPath as $sTag) {
- Utils::Log(LOG_DEBUG, "tag: ".$sTag);
- //!array_key_exists(0, $this->aJson) => element $this->aJson is not a classic array It's an array with defined keys
- if (!array_key_exists(0, $this->aJson) && $sTag != '*') {
- $this->aJson = $this->aJson[$sTag];
- } else {
- $aJsonNew = [];
- foreach ($this->aJson as $aElement) {
- if ($sTag == '*') { //Any tag
- array_push($aJsonNew, $aElement);
- } else {
- if (isset($aElement[$sTag])) {
- array_push($aJsonNew, $aElement[$sTag]);
- }
- }
- }
- $this->aJson = $aJsonNew;
- }
- if (count($this->aJson) == 0) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to find path ".implode("/", $aPath)." until data in json file: $this->sURL $this->sFilePath.");
-
- return false;
- }
- }
- $this->aJsonKey = array_keys($this->aJson);
- if (isset($aParamsSourceJson["fields"])) {
- $this->aFieldsKey = $aParamsSourceJson["fields"];
- }
- if (isset($aParamsSourceJson["FIELDS"])) {
- $this->aFieldsKey = $aParamsSourceJson["FIELDS"];
- }
- Utils::Log(LOG_DEBUG, "aFieldsKey: ".json_encode($this->aFieldsKey));
- Utils::Log(LOG_DEBUG, "aJson: ".json_encode($this->aJson));
- Utils::Log(LOG_DEBUG, "aJsonKey: ".json_encode($this->aJsonKey));
- Utils::Log(LOG_DEBUG, "nb of elements:".count($this->aJson));
-
- $this->iIdx = 0;
-
- return true;
- }
-
- /**
- * Fetch one element from the JSON file
- * The first element is used to check if the columns of the result match the expected "fields"
- *
- * @see Collector::Fetch()
- */
- public function Fetch()
- {
- if (empty($this->aJson)) {
- return false;
- }
- if ($this->iIdx < count($this->aJson)) {
- $aData = $this->aJson[$this->aJsonKey[$this->iIdx]];
- Utils::Log(LOG_DEBUG, '$aData: '.json_encode($aData));
-
- $aDataToSynchronize = $this->SearchFieldValues($aData);
-
- foreach ($this->aSkippedAttributes as $sCode) {
- unset($aDataToSynchronize[$sCode]);
- }
-
- if ($this->iIdx == 0) {
- $this->CheckColumns($aDataToSynchronize, [], 'json file');
- }
- //check if all expected fields are in array. If not add it with null value
- foreach ($this->aCSVHeaders as $sHeader) {
- if (!isset($aDataToSynchronize[$sHeader])) {
- $aDataToSynchronize[$sHeader] = null;
- }
- }
-
- foreach ($this->aNullifiedAttributes as $sHeader) {
- if (!isset($aDataToSynchronize[$sHeader])) {
- $aDataToSynchronize[$sHeader] = null;
- }
- }
-
- $this->iIdx++;
-
- return $aDataToSynchronize;
- }
-
- return false;
- }
-
- /**
- * @param array $aData
- *
- * @return array
- * @throws \Exception
- */
- private function SearchFieldValues($aData, $aTestOnlyFieldsKey = null)
- {
- $aDataToSynchronize = [];
-
- $aCurrentFieldKeys = (is_null($aTestOnlyFieldsKey)) ? $this->aFieldsKey : $aTestOnlyFieldsKey;
- foreach ($aCurrentFieldKeys as $key => $sPath) {
- if ($this->iIdx == 0) {
- Utils::Log(LOG_DEBUG, $key.":".array_search($key, $aCurrentFieldKeys));
- }
- //
- $aJsonKeyPath = explode('/', $sPath);
- $aValue = $this->SearchValue($aJsonKeyPath, $aData);
-
- if (empty($aValue) && array_key_exists($key, $this->aSynchroFieldsToDefaultValues)) {
- $sDefaultValue = $this->aSynchroFieldsToDefaultValues[$key];
- Utils::Log(LOG_DEBUG, "aDataToSynchronize[$key]: $sDefaultValue");
- $aDataToSynchronize[$key] = $sDefaultValue;
- } elseif (! is_null($aValue)) {
- Utils::Log(LOG_DEBUG, "aDataToSynchronize[$key]: $aValue");
- $aDataToSynchronize[$key] = $aValue;
- }
- }
-
- Utils::Log(LOG_DEBUG, '$aDataToSynchronize: '.json_encode($aDataToSynchronize));
- return $aDataToSynchronize;
- }
-
- private function SearchValue($aJsonKeyPath, $aData)
- {
- $sTag = array_shift($aJsonKeyPath);
-
- if ($sTag === '*') {
- foreach ($aData as $sKey => $aDataValue) {
- $aCurrentValue = $this->SearchValue($aJsonKeyPath, $aDataValue);
- if (null !== $aCurrentValue) {
- return $aCurrentValue;
- }
- }
- return null;
- }
-
- if (is_int($sTag)
- && array_is_list($aData)
- && array_key_exists((int) $sTag, $aData)
- ) {
- $aValue = $aData[(int) $sTag];
- } elseif (($sTag != '*')
- && is_array($aData)
- && isset($aData[$sTag])
- ) {
- $aValue = $aData[$sTag];
- } else {
- return null;
- }
-
- if (empty($aJsonKeyPath)) {
- return (is_array($aValue)) ? null : $aValue;
- }
-
- return $this->SearchValue($aJsonKeyPath, $aValue);
- }
-
- /**
- * Determine if a given attribute is allowed to be missing in the data datamodel.
- *
- * The implementation is based on a predefined configuration parameter named from the
- * class of the collector (all lowercase) with _ignored_attributes appended.
- *
- * Example: here is the configuration to "ignore" the attribute 'location_id' for the class MyJSONCollector:
- *
- * location_id
- *
- *
- * @param string $sAttCode
- *
- * @return boolean True if the attribute can be skipped, false otherwise
- */
- public function AttributeIsOptional($sAttCode)
- {
- $aIgnoredAttributes = Utils::GetConfigurationValue(get_class($this)."_ignored_attributes", null);
- if ($aIgnoredAttributes === null) {
- // Try all lowercase
- $aIgnoredAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_ignored_attributes", null);
- }
- if (is_array($aIgnoredAttributes)) {
- if (in_array($sAttCode, $aIgnoredAttributes)) {
- return true;
- }
- }
-
- return parent::AttributeIsOptional($sAttCode);
- }
+ protected $sFileJson;
+ protected $aJson;
+ protected $sURL;
+ protected $sFilePath;
+ protected $aJsonKey;
+ protected $aFieldsKey;
+ protected $sJsonCliCommand;
+ protected $iIdx;
+ protected $aSynchroFieldsToDefaultValues = [];
+
+ /**
+ * Initalization
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ $this->sFileJson = null;
+ $this->sURL = null;
+ $this->aJson = null;
+ $this->aFieldsKey = null;
+ $this->iIdx = 0;
+ }
+
+ /**
+ * Runs the configured query to start fetching the data from the database
+ *
+ * @see Collector::Prepare()
+ */
+ public function Prepare()
+ {
+ $bRet = parent::Prepare();
+ if (!$bRet) {
+ return false;
+ }
+
+ //**** step 1 : get all parameters from config file
+ $aParamsSourceJson = $this->aCollectorConfig;
+ if (isset($aParamsSourceJson["command"])) {
+ $this->sJsonCliCommand = $aParamsSourceJson["command"];
+ }
+ if (isset($aParamsSourceJson["COMMAND"])) {
+ $this->sJsonCliCommand = $aParamsSourceJson["COMMAND"];
+ Utils::Log(LOG_INFO, "[".get_class($this)."] CLI command used is [".$this->sJsonCliCommand."]");
+ }
+
+ // Read the URL or Path from the configuration
+ if (isset($aParamsSourceJson["jsonurl"])) {
+ $this->sURL = $aParamsSourceJson["jsonurl"];
+ }
+ if (isset($aParamsSourceJson["JSONURL"])) {
+ $this->sURL = $aParamsSourceJson["JSONURL"];
+ }
+
+ if ($this->sURL == '') {
+ if (isset($aParamsSourceJson["jsonfile"])) {
+ $this->sFilePath = $aParamsSourceJson["jsonfile"];
+ }
+ if (isset($aParamsSourceJson["JSONFILE"])) { // Try all lowercase
+ $this->sFilePath = $aParamsSourceJson["JSONFILE"];
+ }
+ Utils::Log(LOG_INFO, "Source file path: ".$this->sFilePath);
+ } else {
+ Utils::Log(LOG_INFO, "Source URL: ".$this->sURL);
+ }
+
+ if ($this->sURL == '' && $this->sFilePath == '') {
+ // No query at all !!
+ Utils::Log(LOG_ERR, "[".get_class($this)."] no json URL or path configured! Cannot collect data. Please configure it as '' or '' in the configuration file.");
+
+ return false;
+ }
+ if (array_key_exists('defaults', $aParamsSourceJson)) {
+ if ($aParamsSourceJson['defaults'] !== '') {
+ $this->aSynchroFieldsToDefaultValues = $aParamsSourceJson['defaults'];
+ if (!is_array($this->aSynchroFieldsToDefaultValues)) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] defaults section configuration is not correct. please see documentation.");
+
+ return false;
+ }
+ }
+ }
+
+ $aPath = [];
+ if (isset($aParamsSourceJson["path"])) {
+ $aPath = explode('/', $aParamsSourceJson["path"]);
+ }
+ if (isset($aParamsSourceJson["PATH"])) {
+ $aPath = explode('/', $aParamsSourceJson["PATH"]);
+ }
+ if (count($aPath) == 0) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] no path to find data in JSON file");
+ }
+
+ //**** step 2 : get json file
+ //execute cmd before get the json
+ if (!empty($this->sJsonCliCommand)) {
+ Utils::Exec($this->sJsonCliCommand);
+ }
+
+ //get Json file
+ if ($this->sURL != '') {
+ Utils::Log(LOG_DEBUG, 'Get params for uploading data file ');
+ $aDataGet = [];
+ if (isset($aParamsSourceJson["jsonpost"])) {
+ $aDataGet = $aParamsSourceJson['jsonpost'];
+ } else {
+ $aDataGet = [];
+ }
+ $iSynchroTimeout = (int)Utils::GetConfigurationValue('itop_synchro_timeout', 600); // timeout in seconds, for a synchro to run
+ $aCurlOptions = Utils::GetCurlOptions($iSynchroTimeout);
+
+ //logs
+ Utils::Log(LOG_DEBUG, 'Source aDataGet: '.json_encode($aDataGet));
+ $this->sFileJson = Utils::DoPostRequest($this->sURL, $aDataGet, '', $aResponseHeaders, $aCurlOptions);
+ Utils::Log(LOG_DEBUG, 'Source sFileJson: '.$this->sFileJson);
+ Utils::Log(LOG_INFO, 'Synchro URL (target): '.Utils::GetConfigurationValue('itop_url', []));
+ } else {
+ $this->sFileJson = @file_get_contents($this->sFilePath);
+ if ($this->sFileJson === false) {
+ $this->sFilePath = APPROOT.$this->sFilePath;
+ $this->sFileJson = @file_get_contents($this->sFilePath);
+ }
+ Utils::Log(LOG_DEBUG, 'Source sFileJson: '.$this->sFileJson);
+ Utils::Log(LOG_INFO, 'Synchro URL (target): '.Utils::GetConfigurationValue('itop_url', []));
+ }
+
+ //verify the file
+ if ($this->sFileJson === false) {
+ Utils::Log(LOG_ERR, '['.get_class($this).'] Failed to get JSON file: '.$this->sURL);
+
+ return false;
+ }
+
+ //**** step 3 : read json file
+ $this->aJson = json_decode($this->sFileJson, true);
+ if ($this->aJson == null) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to translate data from JSON file: '".$this->sURL.$this->sFilePath."'. Reason: ".json_last_error_msg());
+
+ return false;
+ }
+
+ //Get table of Element in JSON file with a specific path
+ foreach ($aPath as $sTag) {
+ Utils::Log(LOG_DEBUG, "tag: ".$sTag);
+ //!array_key_exists(0, $this->aJson) => element $this->aJson is not a classic array It's an array with defined keys
+ if (!array_key_exists(0, $this->aJson) && $sTag != '*') {
+ $this->aJson = $this->aJson[$sTag];
+ } else {
+ $aJsonNew = [];
+ foreach ($this->aJson as $aElement) {
+ if ($sTag == '*') { //Any tag
+ array_push($aJsonNew, $aElement);
+ } else {
+ if (isset($aElement[$sTag])) {
+ array_push($aJsonNew, $aElement[$sTag]);
+ }
+ }
+ }
+ $this->aJson = $aJsonNew;
+ }
+ if (count($this->aJson) == 0) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to find path ".implode("/", $aPath)." until data in json file: $this->sURL $this->sFilePath.");
+
+ return false;
+ }
+ }
+ $this->aJsonKey = array_keys($this->aJson);
+ if (isset($aParamsSourceJson["fields"])) {
+ $this->aFieldsKey = $aParamsSourceJson["fields"];
+ }
+ if (isset($aParamsSourceJson["FIELDS"])) {
+ $this->aFieldsKey = $aParamsSourceJson["FIELDS"];
+ }
+ Utils::Log(LOG_DEBUG, "aFieldsKey: ".json_encode($this->aFieldsKey));
+ Utils::Log(LOG_DEBUG, "aJson: ".json_encode($this->aJson));
+ Utils::Log(LOG_DEBUG, "aJsonKey: ".json_encode($this->aJsonKey));
+ Utils::Log(LOG_DEBUG, "nb of elements:".count($this->aJson));
+
+ $this->iIdx = 0;
+
+ return true;
+ }
+
+ /**
+ * Fetch one element from the JSON file
+ * The first element is used to check if the columns of the result match the expected "fields"
+ *
+ * @see Collector::Fetch()
+ */
+ public function Fetch()
+ {
+ if (empty($this->aJson)) {
+ return false;
+ }
+ if ($this->iIdx < count($this->aJson)) {
+ $aData = $this->aJson[$this->aJsonKey[$this->iIdx]];
+ Utils::Log(LOG_DEBUG, '$aData: '.json_encode($aData));
+
+ $aDataToSynchronize = $this->SearchFieldValues($aData);
+
+ foreach ($this->aSkippedAttributes as $sCode) {
+ unset($aDataToSynchronize[$sCode]);
+ }
+
+ if ($this->iIdx == 0) {
+ $this->CheckColumns($aDataToSynchronize, [], 'json file');
+ }
+ //check if all expected fields are in array. If not add it with null value
+ foreach ($this->aCSVHeaders as $sHeader) {
+ if (!isset($aDataToSynchronize[$sHeader])) {
+ $aDataToSynchronize[$sHeader] = null;
+ }
+ }
+
+ foreach ($this->aNullifiedAttributes as $sHeader) {
+ if (!isset($aDataToSynchronize[$sHeader])) {
+ $aDataToSynchronize[$sHeader] = null;
+ }
+ }
+
+ $this->iIdx++;
+
+ return $aDataToSynchronize;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param array $aData
+ *
+ * @return array
+ * @throws \Exception
+ */
+ private function SearchFieldValues($aData, $aTestOnlyFieldsKey = null)
+ {
+ $aDataToSynchronize = [];
+
+ $aCurrentFieldKeys = (is_null($aTestOnlyFieldsKey)) ? $this->aFieldsKey : $aTestOnlyFieldsKey;
+ foreach ($aCurrentFieldKeys as $key => $sPath) {
+ if ($this->iIdx == 0) {
+ Utils::Log(LOG_DEBUG, $key.":".array_search($key, $aCurrentFieldKeys));
+ }
+ //
+ $aJsonKeyPath = explode('/', $sPath);
+ $aValue = $this->SearchValue($aJsonKeyPath, $aData);
+
+ if (empty($aValue) && array_key_exists($key, $this->aSynchroFieldsToDefaultValues)) {
+ $sDefaultValue = $this->aSynchroFieldsToDefaultValues[$key];
+ Utils::Log(LOG_DEBUG, "aDataToSynchronize[$key]: $sDefaultValue");
+ $aDataToSynchronize[$key] = $sDefaultValue;
+ } elseif (! is_null($aValue)) {
+ Utils::Log(LOG_DEBUG, "aDataToSynchronize[$key]: $aValue");
+ $aDataToSynchronize[$key] = $aValue;
+ }
+ }
+
+ Utils::Log(LOG_DEBUG, '$aDataToSynchronize: '.json_encode($aDataToSynchronize));
+ return $aDataToSynchronize;
+ }
+
+ private function SearchValue($aJsonKeyPath, $aData)
+ {
+ $sTag = array_shift($aJsonKeyPath);
+
+ if ($sTag === '*') {
+ foreach ($aData as $sKey => $aDataValue) {
+ $aCurrentValue = $this->SearchValue($aJsonKeyPath, $aDataValue);
+ if (null !== $aCurrentValue) {
+ return $aCurrentValue;
+ }
+ }
+ return null;
+ }
+
+ if (is_int($sTag)
+ && array_is_list($aData)
+ && array_key_exists((int) $sTag, $aData)
+ ) {
+ $aValue = $aData[(int) $sTag];
+ } elseif (($sTag != '*')
+ && is_array($aData)
+ && isset($aData[$sTag])
+ ) {
+ $aValue = $aData[$sTag];
+ } else {
+ return null;
+ }
+
+ if (empty($aJsonKeyPath)) {
+ return (is_array($aValue)) ? null : $aValue;
+ }
+
+ return $this->SearchValue($aJsonKeyPath, $aValue);
+ }
+
+ /**
+ * Determine if a given attribute is allowed to be missing in the data datamodel.
+ *
+ * The implementation is based on a predefined configuration parameter named from the
+ * class of the collector (all lowercase) with _ignored_attributes appended.
+ *
+ * Example: here is the configuration to "ignore" the attribute 'location_id' for the class MyJSONCollector:
+ *
+ * location_id
+ *
+ *
+ * @param string $sAttCode
+ *
+ * @return boolean True if the attribute can be skipped, false otherwise
+ */
+ public function AttributeIsOptional($sAttCode)
+ {
+ $aIgnoredAttributes = Utils::GetConfigurationValue(get_class($this)."_ignored_attributes", null);
+ if ($aIgnoredAttributes === null) {
+ // Try all lowercase
+ $aIgnoredAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_ignored_attributes", null);
+ }
+ if (is_array($aIgnoredAttributes)) {
+ if (in_array($sAttCode, $aIgnoredAttributes)) {
+ return true;
+ }
+ }
+
+ return parent::AttributeIsOptional($sAttCode);
+ }
}
diff --git a/core/lookuptable.class.inc.php b/core/lookuptable.class.inc.php
index be09347..8a89011 100644
--- a/core/lookuptable.class.inc.php
+++ b/core/lookuptable.class.inc.php
@@ -20,179 +20,179 @@
*/
class LookupTable
{
- protected $aData;
- protected $aFieldsPos;
- protected $bCaseSensitive;
- protected $bIgnoreMappingErrors;
- protected $sReturnAttCode;
- protected static $oRestClient;
+ protected $aData;
+ protected $aFieldsPos;
+ protected $bCaseSensitive;
+ protected $bIgnoreMappingErrors;
+ protected $sReturnAttCode;
+ protected static $oRestClient;
- /**
- * @param RestClient $oRestClient
- */
- public static function SetRestClient(RestClient $oRestClient)
- {
- static::$oRestClient = $oRestClient;
- }
+ /**
+ * @param RestClient $oRestClient
+ */
+ public static function SetRestClient(RestClient $oRestClient)
+ {
+ static::$oRestClient = $oRestClient;
+ }
- /**
- * Initialization of a LookupTable, based on an OQL query in iTop
- *
- * @param string $sOQL The OQL query for the objects to integrate in the LookupTable. Format: SELECT [ WHERE ...]
- * @param array $aKeyFields The fields of the object to use in the lookup key
- * @param bool $bCaseSensitive Is the mapping case sensitive ?
- * @param bool $bIgnoreMappingErrors Are mapping errors considered as "normal"? (e.g. when using the lookup table for filtering the data)
- * @param string $sReturnAttCode The attribute code whose value to return as the result of the mapping (by default 'id' meaning the ID of the matching iTop object)
- *
- * @throws Exception
- */
- public function __construct($sOQL, $aKeyFields, $bCaseSensitive = true, $bIgnoreMappingErrors = false, $sReturnAttCode = 'id')
- {
- $this->aData = [];
- $this->aFieldsPos = [];
- $this->bCaseSensitive = $bCaseSensitive;
- $this->bIgnoreMappingErrors = $bIgnoreMappingErrors;
- $this->sReturnAttCode = $sReturnAttCode;
+ /**
+ * Initialization of a LookupTable, based on an OQL query in iTop
+ *
+ * @param string $sOQL The OQL query for the objects to integrate in the LookupTable. Format: SELECT [ WHERE ...]
+ * @param array $aKeyFields The fields of the object to use in the lookup key
+ * @param bool $bCaseSensitive Is the mapping case sensitive ?
+ * @param bool $bIgnoreMappingErrors Are mapping errors considered as "normal"? (e.g. when using the lookup table for filtering the data)
+ * @param string $sReturnAttCode The attribute code whose value to return as the result of the mapping (by default 'id' meaning the ID of the matching iTop object)
+ *
+ * @throws Exception
+ */
+ public function __construct($sOQL, $aKeyFields, $bCaseSensitive = true, $bIgnoreMappingErrors = false, $sReturnAttCode = 'id')
+ {
+ $this->aData = [];
+ $this->aFieldsPos = [];
+ $this->bCaseSensitive = $bCaseSensitive;
+ $this->bIgnoreMappingErrors = $bIgnoreMappingErrors;
+ $this->sReturnAttCode = $sReturnAttCode;
- if (!preg_match('/^SELECT ([^ ]+)/', $sOQL, $aMatches)) {
- throw new Exception("Invalid OQL query: '$sOQL'. Expecting a query starting with 'SELECT xxx'");
- }
- $sClass = $aMatches[1];
- if (static::$oRestClient != null) {
- $oRestClient = static::$oRestClient;
- } else {
- $oRestClient = new RestClient();
- }
- $aRestFields = $aKeyFields;
- if ($this->sReturnAttCode !== 'id') {
- // If the return attcode is not the ID of the object, add it to the list of the required fields
- $aRestFields[] = $this->sReturnAttCode;
- }
- $aRes = $oRestClient->Get($sClass, $sOQL, implode(',', $aRestFields));
- if ($aRes['code'] == 0) {
- foreach ((array)$aRes['objects'] as $sObjKey => $aObj) {
- $iObjKey = 0;
- $aMappingKeys = [];
- foreach ($aKeyFields as $sField) {
- if (!array_key_exists($sField, $aObj['fields'])) {
- Utils::Log(LOG_ERR, "field '$sField' does not exist in '".json_encode($aObj['fields'])."'");
- $aMappingKeys[] = '';
- } else {
- $aMappingKeys[] = $aObj['fields'][$sField];
- }
- }
- $sMappingKey = implode('_', $aMappingKeys);
- if (!$this->bCaseSensitive) {
- if (function_exists('mb_strtolower')) {
- $sMappingKey = mb_strtolower($sMappingKey);
- } else {
- $sMappingKey = strtolower($sMappingKey);
- }
- }
- if ($this->sReturnAttCode !== 'id') {
- // If the return attcode is not the ID of the object, check that it exists
- if (!array_key_exists($this->sReturnAttCode, $aObj['fields'])) {
- Utils::Log(LOG_ERR, "field '{$this->sReturnAttCode}' does not exist in '".json_encode($aObj['fields'])."'");
- $iObjKey = 0;
- } else {
- $iObjKey = $aObj['fields'][$this->sReturnAttCode];
- }
- } else {
- // The return value is the ID of the object
- if (!array_key_exists('key', $aObj)) {
- // Emulate the behavior for older versions of the REST API
- if (preg_match('/::([0-9]+)$/', $sObjKey, $aMatches)) {
- $iObjKey = (int)$aMatches[1];
- }
- } else {
- $iObjKey = (int)$aObj['key'];
- }
- }
- $this->aData[$sMappingKey] = $iObjKey; // Store the mapping
- }
- } else {
- Utils::Log(LOG_ERR, "Unable to retrieve the $sClass objects (query = $sOQL). Message: ".$aRes['message']);
- }
- }
+ if (!preg_match('/^SELECT ([^ ]+)/', $sOQL, $aMatches)) {
+ throw new Exception("Invalid OQL query: '$sOQL'. Expecting a query starting with 'SELECT xxx'");
+ }
+ $sClass = $aMatches[1];
+ if (static::$oRestClient != null) {
+ $oRestClient = static::$oRestClient;
+ } else {
+ $oRestClient = new RestClient();
+ }
+ $aRestFields = $aKeyFields;
+ if ($this->sReturnAttCode !== 'id') {
+ // If the return attcode is not the ID of the object, add it to the list of the required fields
+ $aRestFields[] = $this->sReturnAttCode;
+ }
+ $aRes = $oRestClient->Get($sClass, $sOQL, implode(',', $aRestFields));
+ if ($aRes['code'] == 0) {
+ foreach ((array)$aRes['objects'] as $sObjKey => $aObj) {
+ $iObjKey = 0;
+ $aMappingKeys = [];
+ foreach ($aKeyFields as $sField) {
+ if (!array_key_exists($sField, $aObj['fields'])) {
+ Utils::Log(LOG_ERR, "field '$sField' does not exist in '".json_encode($aObj['fields'])."'");
+ $aMappingKeys[] = '';
+ } else {
+ $aMappingKeys[] = $aObj['fields'][$sField];
+ }
+ }
+ $sMappingKey = implode('_', $aMappingKeys);
+ if (!$this->bCaseSensitive) {
+ if (function_exists('mb_strtolower')) {
+ $sMappingKey = mb_strtolower($sMappingKey);
+ } else {
+ $sMappingKey = strtolower($sMappingKey);
+ }
+ }
+ if ($this->sReturnAttCode !== 'id') {
+ // If the return attcode is not the ID of the object, check that it exists
+ if (!array_key_exists($this->sReturnAttCode, $aObj['fields'])) {
+ Utils::Log(LOG_ERR, "field '{$this->sReturnAttCode}' does not exist in '".json_encode($aObj['fields'])."'");
+ $iObjKey = 0;
+ } else {
+ $iObjKey = $aObj['fields'][$this->sReturnAttCode];
+ }
+ } else {
+ // The return value is the ID of the object
+ if (!array_key_exists('key', $aObj)) {
+ // Emulate the behavior for older versions of the REST API
+ if (preg_match('/::([0-9]+)$/', $sObjKey, $aMatches)) {
+ $iObjKey = (int)$aMatches[1];
+ }
+ } else {
+ $iObjKey = (int)$aObj['key'];
+ }
+ }
+ $this->aData[$sMappingKey] = $iObjKey; // Store the mapping
+ }
+ } else {
+ Utils::Log(LOG_ERR, "Unable to retrieve the $sClass objects (query = $sOQL). Message: ".$aRes['message']);
+ }
+ }
- /**
- * Replaces the given field in the CSV data by the identifier of the object in iTop, based on a list of lookup fields
- *
- * @param hash $aLineData The data corresponding to the line of the CSV file being processed
- * @param array $aLookupFields The list of fields used for the mapping key
- * @param string $sDestField The name of field (i.e. column) to populate with the id of the iTop object
- * @param int $iLineIndex The index of the line (0 = first line of the CSV file)
- *
- * @return bool true if the mapping succeeded, false otherwise
- */
- public function Lookup(&$aLineData, $aLookupFields, $sDestField, $iLineIndex, $bSkipIfEmpty = false)
- {
- $bRet = true;
- if ($iLineIndex == 0) {
- $this->InitLineMappings($aLineData, array_merge($aLookupFields, [$sDestField]));
- } else {
- $iPos = $this->aFieldsPos[$sDestField];
- //skip search if field is empty
- if ($bSkipIfEmpty && $iPos !== null && $aLineData[$iPos] === '') {
- return false;
- }
- $aLookupKey = [];
- foreach ($aLookupFields as $sField) {
- $iPos = $this->aFieldsPos[$sField];
- if ($iPos !== null) {
- $aLookupKey[] = $aLineData[$iPos];
- } else {
- $aLookupKey[] = ''; // missing column ??
- }
- }
- $sLookupKey = implode('_', $aLookupKey);
- if (!$this->bCaseSensitive) {
- if (function_exists('mb_strtolower')) {
- $sLookupKey = mb_strtolower($sLookupKey);
- } else {
- $sLookupKey = strtolower($sLookupKey);
- }
- }
- if (!array_key_exists($sLookupKey, $this->aData)) {
- if ($this->bIgnoreMappingErrors) {
- // Mapping *errors* are expected, just report them in debug mode
- Utils::Log(LOG_DEBUG, "No mapping found with key: '$sLookupKey', '$sDestField' will be set to zero.");
- } else {
- Utils::Log(LOG_WARNING, "No mapping found with key: '$sLookupKey', '$sDestField' will be set to zero.");
- $bRet = false;
- }
- } else {
- $iPos = $this->aFieldsPos[$sDestField];
- if ($iPos !== null) {
- $aLineData[$iPos] = $this->aData[$sLookupKey];
- } else {
- Utils::Log(LOG_WARNING, "'$sDestField' is not a valid column name in the CSV file. Mapping will be ignored.");
- }
- }
- }
+ /**
+ * Replaces the given field in the CSV data by the identifier of the object in iTop, based on a list of lookup fields
+ *
+ * @param hash $aLineData The data corresponding to the line of the CSV file being processed
+ * @param array $aLookupFields The list of fields used for the mapping key
+ * @param string $sDestField The name of field (i.e. column) to populate with the id of the iTop object
+ * @param int $iLineIndex The index of the line (0 = first line of the CSV file)
+ *
+ * @return bool true if the mapping succeeded, false otherwise
+ */
+ public function Lookup(&$aLineData, $aLookupFields, $sDestField, $iLineIndex, $bSkipIfEmpty = false)
+ {
+ $bRet = true;
+ if ($iLineIndex == 0) {
+ $this->InitLineMappings($aLineData, array_merge($aLookupFields, [$sDestField]));
+ } else {
+ $iPos = $this->aFieldsPos[$sDestField];
+ //skip search if field is empty
+ if ($bSkipIfEmpty && $iPos !== null && $aLineData[$iPos] === '') {
+ return false;
+ }
+ $aLookupKey = [];
+ foreach ($aLookupFields as $sField) {
+ $iPos = $this->aFieldsPos[$sField];
+ if ($iPos !== null) {
+ $aLookupKey[] = $aLineData[$iPos];
+ } else {
+ $aLookupKey[] = ''; // missing column ??
+ }
+ }
+ $sLookupKey = implode('_', $aLookupKey);
+ if (!$this->bCaseSensitive) {
+ if (function_exists('mb_strtolower')) {
+ $sLookupKey = mb_strtolower($sLookupKey);
+ } else {
+ $sLookupKey = strtolower($sLookupKey);
+ }
+ }
+ if (!array_key_exists($sLookupKey, $this->aData)) {
+ if ($this->bIgnoreMappingErrors) {
+ // Mapping *errors* are expected, just report them in debug mode
+ Utils::Log(LOG_DEBUG, "No mapping found with key: '$sLookupKey', '$sDestField' will be set to zero.");
+ } else {
+ Utils::Log(LOG_WARNING, "No mapping found with key: '$sLookupKey', '$sDestField' will be set to zero.");
+ $bRet = false;
+ }
+ } else {
+ $iPos = $this->aFieldsPos[$sDestField];
+ if ($iPos !== null) {
+ $aLineData[$iPos] = $this->aData[$sLookupKey];
+ } else {
+ Utils::Log(LOG_WARNING, "'$sDestField' is not a valid column name in the CSV file. Mapping will be ignored.");
+ }
+ }
+ }
- return $bRet;
- }
+ return $bRet;
+ }
- /**
- * Initializes the mapping between the column names (given by the first line of the CSV) and their index, for the given columns
- *
- * @param hash $aLineHeaders An array of strings (the "headers" i.e. first line of the CSV file)
- * @param array $aFields The fields for which a mapping is requested, as an array of strings
- */
- protected function InitLineMappings($aLineHeaders, $aFields)
- {
- foreach ($aLineHeaders as $idx => $sHeader) {
- if (in_array($sHeader, $aFields)) {
- $this->aFieldsPos[$sHeader] = $idx;
- }
- }
+ /**
+ * Initializes the mapping between the column names (given by the first line of the CSV) and their index, for the given columns
+ *
+ * @param hash $aLineHeaders An array of strings (the "headers" i.e. first line of the CSV file)
+ * @param array $aFields The fields for which a mapping is requested, as an array of strings
+ */
+ protected function InitLineMappings($aLineHeaders, $aFields)
+ {
+ foreach ($aLineHeaders as $idx => $sHeader) {
+ if (in_array($sHeader, $aFields)) {
+ $this->aFieldsPos[$sHeader] = $idx;
+ }
+ }
- // Check that all requested fields were found in the headers
- foreach ($aFields as $sField) {
- if (!array_key_exists($sField, $this->aFieldsPos)) {
- Utils::Log(LOG_ERR, "'$sField' is not a valid column name in the CSV file. Mapping will fail.");
- }
- }
- }
+ // Check that all requested fields were found in the headers
+ foreach ($aFields as $sField) {
+ if (!array_key_exists($sField, $this->aFieldsPos)) {
+ Utils::Log(LOG_ERR, "'$sField' is not a valid column name in the CSV file. Mapping will fail.");
+ }
+ }
+ }
}
diff --git a/core/mappingtable.class.inc.php b/core/mappingtable.class.inc.php
index 24f43be..fa8f9a0 100644
--- a/core/mappingtable.class.inc.php
+++ b/core/mappingtable.class.inc.php
@@ -20,61 +20,61 @@
*/
class MappingTable
{
- /**
- * @var string The name of the configuration entry from which the configuratin was loaded
- */
- protected $sConfigEntryName;
+ /**
+ * @var string The name of the configuration entry from which the configuratin was loaded
+ */
+ protected $sConfigEntryName;
- /**
- * @var string[][]
- */
- protected $aMappingTable = [];
+ /**
+ * @var string[][]
+ */
+ protected $aMappingTable = [];
- /**
- * Creates a new MappingTable
- *
- * @param string $sConfigEntryName Name of the XML tag (in the params file) under which the configuration of the mapping table is stored
- */
- public function __construct($sConfigEntryName)
- {
- // Read the "extended mapping" from the configuration
- // The mapping is expressed as an array of strings in the following format:
- $this->sConfigEntryName = $sConfigEntryName;
- $aRawMapping = Utils::GetConfigurationValue($sConfigEntryName, []);
- foreach ($aRawMapping as $sExtendedPattern) {
- $sDelimiter = $sExtendedPattern[0];
- $iEndingDelimiterPos = strrpos($sExtendedPattern, $sDelimiter);
- $sPattern = substr($sExtendedPattern, 0, $iEndingDelimiterPos + 1);
- $sReplacement = substr($sExtendedPattern, $iEndingDelimiterPos + 1);
- $this->aMappingTable[] = [
- 'pattern' => $sPattern,
- 'replacement' => $sReplacement,
- ];
- }
- }
+ /**
+ * Creates a new MappingTable
+ *
+ * @param string $sConfigEntryName Name of the XML tag (in the params file) under which the configuration of the mapping table is stored
+ */
+ public function __construct($sConfigEntryName)
+ {
+ // Read the "extended mapping" from the configuration
+ // The mapping is expressed as an array of strings in the following format:
+ $this->sConfigEntryName = $sConfigEntryName;
+ $aRawMapping = Utils::GetConfigurationValue($sConfigEntryName, []);
+ foreach ($aRawMapping as $sExtendedPattern) {
+ $sDelimiter = $sExtendedPattern[0];
+ $iEndingDelimiterPos = strrpos($sExtendedPattern, $sDelimiter);
+ $sPattern = substr($sExtendedPattern, 0, $iEndingDelimiterPos + 1);
+ $sReplacement = substr($sExtendedPattern, $iEndingDelimiterPos + 1);
+ $this->aMappingTable[] = [
+ 'pattern' => $sPattern,
+ 'replacement' => $sReplacement,
+ ];
+ }
+ }
- /**
- * Normalizes a value through the mapping table
- *
- * @param string $sRawValue The value to normalize
- * @param string $defaultValue Default value if no match is found in the mapping table
- *
- * @return string The normalized value. Can be null if no match is found and no default value was supplied.
- */
- public function MapValue($sRawValue, $defaultValue = null)
- {
- $value = null;
- foreach ($this->aMappingTable as $aMapping) {
- if (preg_match($aMapping['pattern'].'iu', $sRawValue, $aMatches)) { // 'i' for case insensitive matching, 'u' for utf-8 characters
- $value = vsprintf($aMapping['replacement'], $aMatches); // found a suitable match
- Utils::Log(LOG_DEBUG, "MappingTable[{$this->sConfigEntryName}]: input value '$sRawValue' matches '{$aMapping['pattern']}'. Output value is '$value'");
- break;
- }
- }
- if ($value === null) {
- $value = $defaultValue;
- }
+ /**
+ * Normalizes a value through the mapping table
+ *
+ * @param string $sRawValue The value to normalize
+ * @param string $defaultValue Default value if no match is found in the mapping table
+ *
+ * @return string The normalized value. Can be null if no match is found and no default value was supplied.
+ */
+ public function MapValue($sRawValue, $defaultValue = null)
+ {
+ $value = null;
+ foreach ($this->aMappingTable as $aMapping) {
+ if (preg_match($aMapping['pattern'].'iu', $sRawValue, $aMatches)) { // 'i' for case insensitive matching, 'u' for utf-8 characters
+ $value = vsprintf($aMapping['replacement'], $aMatches); // found a suitable match
+ Utils::Log(LOG_DEBUG, "MappingTable[{$this->sConfigEntryName}]: input value '$sRawValue' matches '{$aMapping['pattern']}'. Output value is '$value'");
+ break;
+ }
+ }
+ if ($value === null) {
+ $value = $defaultValue;
+ }
- return $value;
- }
+ return $value;
+ }
}
diff --git a/core/orchestrator.class.inc.php b/core/orchestrator.class.inc.php
index bb4cd06..6a43f21 100644
--- a/core/orchestrator.class.inc.php
+++ b/core/orchestrator.class.inc.php
@@ -17,291 +17,291 @@
class Orchestrator
{
- public static $aCollectors = [];
- public static $aMinVersions = ['PHP' => '7.0', 'simplexml' => '7.0', 'dom' => '1'];
+ public static $aCollectors = [];
+ public static $aMinVersions = ['PHP' => '7.0', 'simplexml' => '7.0', 'dom' => '1'];
- /**
- * Add a collector class to be run in the specified order
- *
- * @param float $fExecOrder The execution order (smaller numbers run first)
- * @param string $sCollectorClass The class name of the collector. Must be a subclass of {@link Collector}
- *
- * @return void
- * @throws Exception
- */
- public static function AddCollector($fExecOrder, $sCollectorClass)
- {
- $oReflection = new ReflectionClass($sCollectorClass);
- if (!$oReflection->IsSubclassOf('Collector')) {
- throw new Exception('Cannot register a collector class ('.$sCollectorClass.') which is not derived from Collector.');
- }
- if ($oReflection->IsAbstract()) {
- throw new Exception('Cannot register an abstract class ('.$sCollectorClass.') as a collector.');
- }
- self::$aCollectors[$sCollectorClass] = ['order' => $fExecOrder, 'class' => $sCollectorClass, 'sds_name' => '', 'sds_id' => 0];
- }
+ /**
+ * Add a collector class to be run in the specified order
+ *
+ * @param float $fExecOrder The execution order (smaller numbers run first)
+ * @param string $sCollectorClass The class name of the collector. Must be a subclass of {@link Collector}
+ *
+ * @return void
+ * @throws Exception
+ */
+ public static function AddCollector($fExecOrder, $sCollectorClass)
+ {
+ $oReflection = new ReflectionClass($sCollectorClass);
+ if (!$oReflection->IsSubclassOf('Collector')) {
+ throw new Exception('Cannot register a collector class ('.$sCollectorClass.') which is not derived from Collector.');
+ }
+ if ($oReflection->IsAbstract()) {
+ throw new Exception('Cannot register an abstract class ('.$sCollectorClass.') as a collector.');
+ }
+ self::$aCollectors[$sCollectorClass] = ['order' => $fExecOrder, 'class' => $sCollectorClass, 'sds_name' => '', 'sds_id' => 0];
+ }
- /**
- * @param string $sCollectionPlanClass The class name of the CollectionPlan. Must be a subclass of {@link CollectionPlan}
- *
- * @return void
- * @throws \ReflectionException
- */
- public static function UseCollectionPlan($sCollectionPlanClass)
- {
- $oReflection = new ReflectionClass($sCollectionPlanClass);
- if (!$oReflection->IsSubclassOf(CollectionPlan::class)) {
- throw new Exception('Cannot register a CollectionPlan class ('.$sCollectionPlanClass.') which is not derived from CollectionPlan.');
- }
- if ($oReflection->IsAbstract()) {
- throw new Exception('Cannot register an CollectionPlan class ('.$sCollectionPlanClass.') as a CollectionPlan.');
- }
- /** @var CollectionPlan $oCollectionPlan */
- $oCollectionPlan = new $sCollectionPlanClass();
- $oCollectionPlan->Init();
- $oCollectionPlan->AddCollectorsToOrchestrator();
- }
+ /**
+ * @param string $sCollectionPlanClass The class name of the CollectionPlan. Must be a subclass of {@link CollectionPlan}
+ *
+ * @return void
+ * @throws \ReflectionException
+ */
+ public static function UseCollectionPlan($sCollectionPlanClass)
+ {
+ $oReflection = new ReflectionClass($sCollectionPlanClass);
+ if (!$oReflection->IsSubclassOf(CollectionPlan::class)) {
+ throw new Exception('Cannot register a CollectionPlan class ('.$sCollectionPlanClass.') which is not derived from CollectionPlan.');
+ }
+ if ($oReflection->IsAbstract()) {
+ throw new Exception('Cannot register an CollectionPlan class ('.$sCollectionPlanClass.') as a CollectionPlan.');
+ }
+ /** @var CollectionPlan $oCollectionPlan */
+ $oCollectionPlan = new $sCollectionPlanClass();
+ $oCollectionPlan->Init();
+ $oCollectionPlan->AddCollectorsToOrchestrator();
+ }
- /**
- * Specify a requirement for a minimum version: either for PHP or for a specific extension
- *
- * @param string $sMinRequiredVersion The minimum version number required
- * @param string $sExtension The name of the extension, if not specified, then the requirement is for the PHP version itself
- *
- * @return void
- */
- public static function AddRequirement($sMinRequiredVersion, $sExtension = 'PHP')
- {
- if (!array_key_exists($sExtension, self::$aMinVersions)) {
- // This is the first call to add some requirements for this extension, record it as-is
- self::$aMinVersions[$sExtension] = $sMinRequiredVersion;
- } elseif (version_compare($sMinRequiredVersion, self::$aMinVersions[$sExtension], '>')) {
- // This requirement is stricter than the previously requested one
- self::$aMinVersions[$sExtension] = $sMinRequiredVersion;
- }
- }
+ /**
+ * Specify a requirement for a minimum version: either for PHP or for a specific extension
+ *
+ * @param string $sMinRequiredVersion The minimum version number required
+ * @param string $sExtension The name of the extension, if not specified, then the requirement is for the PHP version itself
+ *
+ * @return void
+ */
+ public static function AddRequirement($sMinRequiredVersion, $sExtension = 'PHP')
+ {
+ if (!array_key_exists($sExtension, self::$aMinVersions)) {
+ // This is the first call to add some requirements for this extension, record it as-is
+ self::$aMinVersions[$sExtension] = $sMinRequiredVersion;
+ } elseif (version_compare($sMinRequiredVersion, self::$aMinVersions[$sExtension], '>')) {
+ // This requirement is stricter than the previously requested one
+ self::$aMinVersions[$sExtension] = $sMinRequiredVersion;
+ }
+ }
- /**
- * Check that all specified requirements are met, and log (LOG_ERR if not met, LOG_DEBUG if Ok)
- *
- * @return boolean True if it's Ok, false otherwise
- */
- public static function CheckRequirements()
- {
- $bResult = true;
- foreach (self::$aMinVersions as $sExtension => $sRequiredVersion) {
- if ($sExtension == 'PHP') {
- $sCurrentVersion = phpversion();
- if (version_compare($sCurrentVersion, $sRequiredVersion, '<')) {
- $bResult = false;
- Utils::Log(LOG_ERR, "The required PHP version to run this application is $sRequiredVersion. The current PHP version is only $sCurrentVersion.");
- } else {
- Utils::Log(LOG_DEBUG, "OK, the required PHP version to run this application is $sRequiredVersion. The current PHP version is $sCurrentVersion.");
- }
- } elseif (extension_loaded($sExtension)) {
- $sCurrentVersion = phpversion($sExtension);
- if (version_compare($sCurrentVersion, $sRequiredVersion, '<')) {
- $bResult = false;
- Utils::Log(LOG_ERR, "The extension '$sExtension' (version >= $sRequiredVersion) is required to run this application. The installed version is only $sCurrentVersion.");
- } else {
- Utils::Log(LOG_DEBUG, "OK, the required extension '$sExtension' is installed (current version: $sCurrentVersion >= $sRequiredVersion).");
- }
+ /**
+ * Check that all specified requirements are met, and log (LOG_ERR if not met, LOG_DEBUG if Ok)
+ *
+ * @return boolean True if it's Ok, false otherwise
+ */
+ public static function CheckRequirements()
+ {
+ $bResult = true;
+ foreach (self::$aMinVersions as $sExtension => $sRequiredVersion) {
+ if ($sExtension == 'PHP') {
+ $sCurrentVersion = phpversion();
+ if (version_compare($sCurrentVersion, $sRequiredVersion, '<')) {
+ $bResult = false;
+ Utils::Log(LOG_ERR, "The required PHP version to run this application is $sRequiredVersion. The current PHP version is only $sCurrentVersion.");
+ } else {
+ Utils::Log(LOG_DEBUG, "OK, the required PHP version to run this application is $sRequiredVersion. The current PHP version is $sCurrentVersion.");
+ }
+ } elseif (extension_loaded($sExtension)) {
+ $sCurrentVersion = phpversion($sExtension);
+ if (version_compare($sCurrentVersion, $sRequiredVersion, '<')) {
+ $bResult = false;
+ Utils::Log(LOG_ERR, "The extension '$sExtension' (version >= $sRequiredVersion) is required to run this application. The installed version is only $sCurrentVersion.");
+ } else {
+ Utils::Log(LOG_DEBUG, "OK, the required extension '$sExtension' is installed (current version: $sCurrentVersion >= $sRequiredVersion).");
+ }
- } else {
- $bResult = false;
- Utils::Log(LOG_ERR, "The missing extension '$sExtension' (version >= $sRequiredVersion) is required to run this application.");
- }
- }
+ } else {
+ $bResult = false;
+ Utils::Log(LOG_ERR, "The missing extension '$sExtension' (version >= $sRequiredVersion) is required to run this application.");
+ }
+ }
- return $bResult;
- }
+ return $bResult;
+ }
- /**
- * Returns the list of registered collectors, sorted in their execution order
- *
- * @return array An array of Collector instances
- */
- public function ListCollectors()
- {
- $aResults = [];
- //Sort the collectors based on their order
- uasort(self::$aCollectors, ["Orchestrator", "CompareCollectors"]);
+ /**
+ * Returns the list of registered collectors, sorted in their execution order
+ *
+ * @return array An array of Collector instances
+ */
+ public function ListCollectors()
+ {
+ $aResults = [];
+ //Sort the collectors based on their order
+ uasort(self::$aCollectors, ["Orchestrator", "CompareCollectors"]);
- foreach (self::$aCollectors as $aCollectorData) {
- /** @var Collector $oClass */
- $oClass = new $aCollectorData['class']();
- $oClass->Init();
- $aResults[] = $oClass;
- //$aResults[] = new $aCollectorData['class']();
- }
+ foreach (self::$aCollectors as $aCollectorData) {
+ /** @var Collector $oClass */
+ $oClass = new $aCollectorData['class']();
+ $oClass->Init();
+ $aResults[] = $oClass;
+ //$aResults[] = new $aCollectorData['class']();
+ }
- return $aResults;
- }
+ return $aResults;
+ }
- /**
- * Initializes the synchronization data sources in iTop, according to the collectors' JSON specifications
- *
- * @param string[] $aCollectors list of classes implementing {@link Collector}
- *
- * @return boolean True if Ok, false otherwise
- * @throws \InvalidConfigException
- */
- public function InitSynchroDataSources($aCollectors)
- {
- $bResult = true;
- $aPlaceholders = [];
- $sEmailToNotify = Utils::GetConfigurationValue('contact_to_notify', '');
- $aPlaceholders['$contact_to_notify$'] = 0;
- if ($sEmailToNotify != '') {
- $oRestClient = new RestClient();
- $aRes = $oRestClient->Get('Contact', ['email' => $sEmailToNotify]);
- if ($aRes['code'] == 0) {
- if (!is_array($aRes['objects'])) {
- Utils::Log(LOG_WARNING, "Contact to notify ($sEmailToNotify) not found in iTop. Nobody will be notified of the results of the synchronization.");
- } else {
- foreach ($aRes['objects'] as $sKey => $aObj) {
- if (!array_key_exists('key', $aObj)) {
- // Emulate the behavior for older versions of the API
- if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
- $aPlaceholders['$contact_to_notify$'] = (int)$aMatches[1];
- }
- } else {
- $aPlaceholders['$contact_to_notify$'] = (int)$aObj['key'];
- }
- Utils::Log(LOG_INFO, "Contact to notify: '{$aObj['fields']['friendlyname']}' <{$aObj['fields']['email']}> ({$aPlaceholders['$contact_to_notify$']}).");
- break;
- }
- }
- } else {
- Utils::Log(LOG_ERR, "Unable to find the contact with email = '$sEmailToNotify'. No contact to notify will be defined.");
- }
- }
- $sSynchroUser = Utils::GetConfigurationValue('synchro_user') ?: Utils::GetConfigurationValue('itop_login');
- $aPlaceholders['$synchro_user$'] = 0;
- if ($sSynchroUser != '') {
- $oRestClient = new RestClient();
- $aRes = $oRestClient->Get('User', ['login' => $sSynchroUser]);
- if ($aRes['code'] == 0) {
- foreach ($aRes['objects'] as $sKey => $aObj) {
- if (!array_key_exists('key', $aObj)) {
- // Emulate the behavior for older versions of the API
- if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
- $aPlaceholders['$synchro_user$'] = (int)$aMatches[1];
- }
- } else {
- $aPlaceholders['$synchro_user$'] = (int)$aObj['key'];
- }
- Utils::Log(LOG_INFO, "Synchro User: '{$aObj['fields']['friendlyname']}' <{$aObj['fields']['email']}> ({$aPlaceholders['$synchro_user$']}).");
- break;
- }
- } else {
- if (array_key_exists('message', $aRes)) {
- Utils::Log(LOG_ERR, "Unable to use synchro user with login = '$sSynchroUser'. ".$aRes['message']);
- } else {
- Utils::Log(LOG_ERR, "Unable to use synchro user with login = '$sSynchroUser'. No user is defined.");
- }
- }
- }
- $aOtherPlaceholders = Utils::GetConfigurationValue('json_placeholders', []);
+ /**
+ * Initializes the synchronization data sources in iTop, according to the collectors' JSON specifications
+ *
+ * @param string[] $aCollectors list of classes implementing {@link Collector}
+ *
+ * @return boolean True if Ok, false otherwise
+ * @throws \InvalidConfigException
+ */
+ public function InitSynchroDataSources($aCollectors)
+ {
+ $bResult = true;
+ $aPlaceholders = [];
+ $sEmailToNotify = Utils::GetConfigurationValue('contact_to_notify', '');
+ $aPlaceholders['$contact_to_notify$'] = 0;
+ if ($sEmailToNotify != '') {
+ $oRestClient = new RestClient();
+ $aRes = $oRestClient->Get('Contact', ['email' => $sEmailToNotify]);
+ if ($aRes['code'] == 0) {
+ if (!is_array($aRes['objects'])) {
+ Utils::Log(LOG_WARNING, "Contact to notify ($sEmailToNotify) not found in iTop. Nobody will be notified of the results of the synchronization.");
+ } else {
+ foreach ($aRes['objects'] as $sKey => $aObj) {
+ if (!array_key_exists('key', $aObj)) {
+ // Emulate the behavior for older versions of the API
+ if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
+ $aPlaceholders['$contact_to_notify$'] = (int)$aMatches[1];
+ }
+ } else {
+ $aPlaceholders['$contact_to_notify$'] = (int)$aObj['key'];
+ }
+ Utils::Log(LOG_INFO, "Contact to notify: '{$aObj['fields']['friendlyname']}' <{$aObj['fields']['email']}> ({$aPlaceholders['$contact_to_notify$']}).");
+ break;
+ }
+ }
+ } else {
+ Utils::Log(LOG_ERR, "Unable to find the contact with email = '$sEmailToNotify'. No contact to notify will be defined.");
+ }
+ }
+ $sSynchroUser = Utils::GetConfigurationValue('synchro_user') ?: Utils::GetConfigurationValue('itop_login');
+ $aPlaceholders['$synchro_user$'] = 0;
+ if ($sSynchroUser != '') {
+ $oRestClient = new RestClient();
+ $aRes = $oRestClient->Get('User', ['login' => $sSynchroUser]);
+ if ($aRes['code'] == 0) {
+ foreach ($aRes['objects'] as $sKey => $aObj) {
+ if (!array_key_exists('key', $aObj)) {
+ // Emulate the behavior for older versions of the API
+ if (preg_match('/::([0-9]+)$/', $sKey, $aMatches)) {
+ $aPlaceholders['$synchro_user$'] = (int)$aMatches[1];
+ }
+ } else {
+ $aPlaceholders['$synchro_user$'] = (int)$aObj['key'];
+ }
+ Utils::Log(LOG_INFO, "Synchro User: '{$aObj['fields']['friendlyname']}' <{$aObj['fields']['email']}> ({$aPlaceholders['$synchro_user$']}).");
+ break;
+ }
+ } else {
+ if (array_key_exists('message', $aRes)) {
+ Utils::Log(LOG_ERR, "Unable to use synchro user with login = '$sSynchroUser'. ".$aRes['message']);
+ } else {
+ Utils::Log(LOG_ERR, "Unable to use synchro user with login = '$sSynchroUser'. No user is defined.");
+ }
+ }
+ }
+ $aOtherPlaceholders = Utils::GetConfigurationValue('json_placeholders', []);
- if (is_array($aOtherPlaceholders)) {
- foreach ($aOtherPlaceholders as $sKey => $sValue) {
- $aPlaceholders['$'.$sKey.'$'] = $sValue;
- }
- }
+ if (is_array($aOtherPlaceholders)) {
+ foreach ($aOtherPlaceholders as $sKey => $sValue) {
+ $aPlaceholders['$'.$sKey.'$'] = $sValue;
+ }
+ }
- /** @var \Collector $oCollector */
- foreach ($aCollectors as $oCollector) {
- Utils::SetCollector($oCollector, "InitSynchroDataSource");
- $bResult = $oCollector->InitSynchroDataSource($aPlaceholders);
- if (!$bResult) {
- break;
- }
- }
- Utils::SetCollector(null);
+ /** @var \Collector $oCollector */
+ foreach ($aCollectors as $oCollector) {
+ Utils::SetCollector($oCollector, "InitSynchroDataSource");
+ $bResult = $oCollector->InitSynchroDataSource($aPlaceholders);
+ if (!$bResult) {
+ break;
+ }
+ }
+ Utils::SetCollector(null);
- return $bResult;
- }
+ return $bResult;
+ }
- /**
- * Run the first pass of data collection: fetching the raw data from inventory scripts
- *
- * @param string[] $aCollectors list of classes implementing {@link Collector}
- * @param int $iMaxChunkSize
- * @param boolean $bCollectOnly
- *
- * @return boolean True if Ok, false otherwise
- */
- public function Collect($aCollectors, $iMaxChunkSize, $bCollectOnly)
- {
- $bResult = true;
- /** @var \Collector $oCollector */
- foreach ($aCollectors as $oCollector) {
- Utils::SetCollector($oCollector, "Collect");
- $bResult = $oCollector->Collect($iMaxChunkSize, $bCollectOnly);
- if (!$bResult) {
- break;
- }
- }
- Utils::SetCollector(null);
+ /**
+ * Run the first pass of data collection: fetching the raw data from inventory scripts
+ *
+ * @param string[] $aCollectors list of classes implementing {@link Collector}
+ * @param int $iMaxChunkSize
+ * @param boolean $bCollectOnly
+ *
+ * @return boolean True if Ok, false otherwise
+ */
+ public function Collect($aCollectors, $iMaxChunkSize, $bCollectOnly)
+ {
+ $bResult = true;
+ /** @var \Collector $oCollector */
+ foreach ($aCollectors as $oCollector) {
+ Utils::SetCollector($oCollector, "Collect");
+ $bResult = $oCollector->Collect($iMaxChunkSize, $bCollectOnly);
+ if (!$bResult) {
+ break;
+ }
+ }
+ Utils::SetCollector(null);
- return $bResult;
- }
+ return $bResult;
+ }
- /**
- * Run the final pass of the collection: synchronizing the data into iTop
- *
- * @param string[] $aCollectors list of classes implementing {@link Collector}
- *
- * @return boolean
- */
- public function Synchronize($aCollectors)
- {
- $bResult = true;
- $sStopOnError = Utils::GetConfigurationValue('stop_on_synchro_error', 'no');
- if (($sStopOnError != 'yes') && ($sStopOnError != 'no')) {
- Utils::Log(LOG_WARNING, "Unexpected value '$sStopOnError' for the parameter 'stop_on_synchro_error'. Will NOT stop on error. The expected values for this parameter are 'yes' or 'no'.");
- }
- $bStopOnError = ($sStopOnError == 'yes');
- /** @var \Collector $oCollector */
- foreach ($aCollectors as $oCollector) {
- Utils::SetCollector($oCollector, "Synchronize");
- $bResult = $oCollector->Synchronize();
- if (!$bResult) {
- if ($bStopOnError) {
- break;
- } else {
- // Do not report the error (it impacts the return code of the process)
- $bResult = true;
- }
- }
- }
- Utils::SetCollector(null);
+ /**
+ * Run the final pass of the collection: synchronizing the data into iTop
+ *
+ * @param string[] $aCollectors list of classes implementing {@link Collector}
+ *
+ * @return boolean
+ */
+ public function Synchronize($aCollectors)
+ {
+ $bResult = true;
+ $sStopOnError = Utils::GetConfigurationValue('stop_on_synchro_error', 'no');
+ if (($sStopOnError != 'yes') && ($sStopOnError != 'no')) {
+ Utils::Log(LOG_WARNING, "Unexpected value '$sStopOnError' for the parameter 'stop_on_synchro_error'. Will NOT stop on error. The expected values for this parameter are 'yes' or 'no'.");
+ }
+ $bStopOnError = ($sStopOnError == 'yes');
+ /** @var \Collector $oCollector */
+ foreach ($aCollectors as $oCollector) {
+ Utils::SetCollector($oCollector, "Synchronize");
+ $bResult = $oCollector->Synchronize();
+ if (!$bResult) {
+ if ($bStopOnError) {
+ break;
+ } else {
+ // Do not report the error (it impacts the return code of the process)
+ $bResult = true;
+ }
+ }
+ }
+ Utils::SetCollector(null);
- return $bResult;
- }
+ return $bResult;
+ }
- /////////////////////////////////////////////////////////////////////////
- //
- // Internal methods
- //
- /////////////////////////////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////
+ //
+ // Internal methods
+ //
+ /////////////////////////////////////////////////////////////////////////
- /**
- * Helper callback for sorting the collectors using the built-in uasort function
- *
- * @param array $aCollector1
- * @param array $aCollector2
- *
- * @return number
- */
- public static function CompareCollectors($aCollector1, $aCollector2)
- {
- if ($aCollector1['order'] == $aCollector2['order']) {
- return 0;
- }
+ /**
+ * Helper callback for sorting the collectors using the built-in uasort function
+ *
+ * @param array $aCollector1
+ * @param array $aCollector2
+ *
+ * @return number
+ */
+ public static function CompareCollectors($aCollector1, $aCollector2)
+ {
+ if ($aCollector1['order'] == $aCollector2['order']) {
+ return 0;
+ }
- return ($aCollector1['order'] > $aCollector2['order']) ? +1 : -1;
- }
+ return ($aCollector1['order'] > $aCollector2['order']) ? +1 : -1;
+ }
}
diff --git a/core/parameters.class.inc.php b/core/parameters.class.inc.php
index c63df34..564a22e 100644
--- a/core/parameters.class.inc.php
+++ b/core/parameters.class.inc.php
@@ -25,215 +25,215 @@ class InvalidParameterException extends Exception
*/
class Parameters
{
- protected $aData = null;
- protected $sParametersFile;
-
- public function __construct($sInputFile = null)
- {
- $this->aData = [];
- if ($sInputFile != null) {
- $this->LoadFromFile($sInputFile);
- }
- }
-
- public function Get($sCode, $default = '')
- {
- if (array_key_exists($sCode, $this->aData)) {
- return $this->aData[$sCode];
- }
-
- return $default;
- }
-
- public function Set($sCode, $value)
- {
- $this->aData[$sCode] = $value;
- }
-
- protected function ToXML(DOMNode $oRoot, $data = null, $sNodeName = null)
- {
- if ($data === null) {
- $data = $this->aData;
- }
-
- if ($oRoot instanceof DOMDocument) {
- $oNode = $oRoot->createElement($sNodeName);
- } else {
- $oNode = $oRoot->ownerDocument->createElement($sNodeName);
- }
- $oRoot->appendChild($oNode);
-
- if (is_array($data)) {
-
- $aKeys = array_keys($data);
- $bNumericKeys = true;
- foreach ($aKeys as $idx => $subkey) {
- if (((int)$subkey) !== $subkey) {
- $bNumericKeys = false;
- break;
- }
- }
- if ($bNumericKeys) {
- $oNode->setAttribute("type", "array");
- foreach ($data as $key => $value) {
- $this->ToXML($oNode, $value, 'item');
- }
- } else {
- foreach ($data as $key => $value) {
- $this->ToXML($oNode, $value, $key);
- }
- }
- } else {
- $oTextNode = $oRoot->ownerDocument->createTextNode($data);
- $oNode->appendChild($oTextNode);
- }
-
- return $oNode;
- }
-
- public function SaveToFile($sFileName)
- {
- $oDoc = new DOMDocument('1.0', 'UTF-8');
- $oDoc->preserveWhiteSpace = false;
- $oDoc->formatOutput = true;
- $this->ToXML($oDoc, null, 'parameters');
- $oDoc->save($sFileName);
- }
-
- public function Dump()
- {
- $oDoc = new DOMDocument('1.0', 'UTF-8');
- $oDoc->preserveWhiteSpace = false;
- $oDoc->formatOutput = true;
- $this->ToXML($oDoc, null, 'parameters');
-
- return $oDoc->saveXML();
- }
-
- public function LoadFromFile($sParametersFile)
- {
- $this->sParametersFile = $sParametersFile;
- if ($this->aData == null) {
- libxml_use_internal_errors(true);
- $oXML = @simplexml_load_file($this->sParametersFile);
- if (!$oXML) {
- $aMessage = [];
- foreach (libxml_get_errors() as $oError) {
- $aMessage[] = "(line: {$oError->line}) ".$oError->message; // Beware: $oError->columns sometimes returns wrong (misleading) value
- }
- libxml_clear_errors();
- throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': ".implode(' ', $aMessage));
- }
-
- $this->aData = [];
- foreach ($oXML as $key => $oElement) {
- $this->aData[(string)$key] = $this->ReadElement($oElement);
- }
- }
- }
-
- protected function ReadElement(SimpleXMLElement $oElement)
- {
- $sDefaultNodeType = (count($oElement->children()) > 0) ? 'hash' : 'string';
- $sNodeType = $this->GetAttribute('type', $oElement, $sDefaultNodeType);
- switch ($sNodeType) {
- case 'array':
- $value = [];
- // Treat the current element as zero based array, child tag names are NOT meaningful
- $sFirstTagName = null;
- foreach ($oElement->children() as $oChildElement) {
- if ($sFirstTagName == null) {
- $sFirstTagName = $oChildElement->getName();
- } elseif ($sFirstTagName != $oChildElement->getName()) {
- throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': mixed tags ('$sFirstTagName' and '".$oChildElement->getName()."') inside array '".$oElement->getName()."'");
- }
- $val = $this->ReadElement($oChildElement);
- $value[] = $val;
- }
- break;
-
- case 'hash':
- $value = [];
- // Treat the current element as a hash, child tag names are keys
- foreach ($oElement->children() as $oChildElement) {
- if (array_key_exists($oChildElement->getName(), $value)) {
- throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': duplicate tags '".$oChildElement->getName()."' inside hash '".$oElement->getName()."'");
- }
- $val = $this->ReadElement($oChildElement);
- $value[$oChildElement->getName()] = $val;
- }
- break;
-
- case 'int':
- case 'integer':
- $value = (int)$oElement;
- break;
-
- case 'string':
- default:
- $value = (string)$oElement;
- }
-
- return $value;
- }
-
- protected function GetAttribute($sAttName, $oElement, $sDefaultValue)
- {
- $sRet = $sDefaultValue;
-
- foreach ($oElement->attributes() as $sKey => $oChildElement) {
- if ((string)$sKey == $sAttName) {
- $sRet = (string)$oChildElement;
- break;
- }
- }
-
- return $sRet;
- }
-
- public function Merge(Parameters $oTask)
- {
- $this->aData = $this->array_merge_recursive_distinct($this->aData, $oTask->aData);
- }
-
- /**
- * array_merge_recursive does indeed merge arrays, but it converts values with duplicate
- * keys to arrays rather than overwriting the value in the first array with the duplicate
- * value in the second array, as array_merge does. I.e., with array_merge_recursive,
- * this happens (documented behavior):
- *
- * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
- * => array('key' => array('org value', 'new value'));
- *
- * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
- * Matching keys' values in the second array overwrite those in the first array, as is the
- * case with array_merge, i.e.:
- *
- * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
- * => array('key' => array('new value'));
- *
- * Parameters are passed by reference, though only for performance reasons. They're not
- * altered by this function.
- *
- * @param array $array1
- * @param array $array2
- *
- * @return array
- * @author Daniel
- * @author Gabriel Sobrinho
- */
- protected function array_merge_recursive_distinct(array &$array1, array &$array2)
- {
- $merged = $array1;
-
- foreach ($array2 as $key => &$value) {
- if (is_array($value) && isset($merged [$key]) && is_array($merged [$key])) {
- $merged [$key] = $this->array_merge_recursive_distinct($merged [$key], $value);
- } else {
- $merged [$key] = $value;
- }
- }
-
- return $merged;
- }
+ protected $aData = null;
+ protected $sParametersFile;
+
+ public function __construct($sInputFile = null)
+ {
+ $this->aData = [];
+ if ($sInputFile != null) {
+ $this->LoadFromFile($sInputFile);
+ }
+ }
+
+ public function Get($sCode, $default = '')
+ {
+ if (array_key_exists($sCode, $this->aData)) {
+ return $this->aData[$sCode];
+ }
+
+ return $default;
+ }
+
+ public function Set($sCode, $value)
+ {
+ $this->aData[$sCode] = $value;
+ }
+
+ protected function ToXML(DOMNode $oRoot, $data = null, $sNodeName = null)
+ {
+ if ($data === null) {
+ $data = $this->aData;
+ }
+
+ if ($oRoot instanceof DOMDocument) {
+ $oNode = $oRoot->createElement($sNodeName);
+ } else {
+ $oNode = $oRoot->ownerDocument->createElement($sNodeName);
+ }
+ $oRoot->appendChild($oNode);
+
+ if (is_array($data)) {
+
+ $aKeys = array_keys($data);
+ $bNumericKeys = true;
+ foreach ($aKeys as $idx => $subkey) {
+ if (((int)$subkey) !== $subkey) {
+ $bNumericKeys = false;
+ break;
+ }
+ }
+ if ($bNumericKeys) {
+ $oNode->setAttribute("type", "array");
+ foreach ($data as $key => $value) {
+ $this->ToXML($oNode, $value, 'item');
+ }
+ } else {
+ foreach ($data as $key => $value) {
+ $this->ToXML($oNode, $value, $key);
+ }
+ }
+ } else {
+ $oTextNode = $oRoot->ownerDocument->createTextNode($data);
+ $oNode->appendChild($oTextNode);
+ }
+
+ return $oNode;
+ }
+
+ public function SaveToFile($sFileName)
+ {
+ $oDoc = new DOMDocument('1.0', 'UTF-8');
+ $oDoc->preserveWhiteSpace = false;
+ $oDoc->formatOutput = true;
+ $this->ToXML($oDoc, null, 'parameters');
+ $oDoc->save($sFileName);
+ }
+
+ public function Dump()
+ {
+ $oDoc = new DOMDocument('1.0', 'UTF-8');
+ $oDoc->preserveWhiteSpace = false;
+ $oDoc->formatOutput = true;
+ $this->ToXML($oDoc, null, 'parameters');
+
+ return $oDoc->saveXML();
+ }
+
+ public function LoadFromFile($sParametersFile)
+ {
+ $this->sParametersFile = $sParametersFile;
+ if ($this->aData == null) {
+ libxml_use_internal_errors(true);
+ $oXML = @simplexml_load_file($this->sParametersFile);
+ if (!$oXML) {
+ $aMessage = [];
+ foreach (libxml_get_errors() as $oError) {
+ $aMessage[] = "(line: {$oError->line}) ".$oError->message; // Beware: $oError->columns sometimes returns wrong (misleading) value
+ }
+ libxml_clear_errors();
+ throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': ".implode(' ', $aMessage));
+ }
+
+ $this->aData = [];
+ foreach ($oXML as $key => $oElement) {
+ $this->aData[(string)$key] = $this->ReadElement($oElement);
+ }
+ }
+ }
+
+ protected function ReadElement(SimpleXMLElement $oElement)
+ {
+ $sDefaultNodeType = (count($oElement->children()) > 0) ? 'hash' : 'string';
+ $sNodeType = $this->GetAttribute('type', $oElement, $sDefaultNodeType);
+ switch ($sNodeType) {
+ case 'array':
+ $value = [];
+ // Treat the current element as zero based array, child tag names are NOT meaningful
+ $sFirstTagName = null;
+ foreach ($oElement->children() as $oChildElement) {
+ if ($sFirstTagName == null) {
+ $sFirstTagName = $oChildElement->getName();
+ } elseif ($sFirstTagName != $oChildElement->getName()) {
+ throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': mixed tags ('$sFirstTagName' and '".$oChildElement->getName()."') inside array '".$oElement->getName()."'");
+ }
+ $val = $this->ReadElement($oChildElement);
+ $value[] = $val;
+ }
+ break;
+
+ case 'hash':
+ $value = [];
+ // Treat the current element as a hash, child tag names are keys
+ foreach ($oElement->children() as $oChildElement) {
+ if (array_key_exists($oChildElement->getName(), $value)) {
+ throw new InvalidParameterException("Invalid Parameters file '{$this->sParametersFile}': duplicate tags '".$oChildElement->getName()."' inside hash '".$oElement->getName()."'");
+ }
+ $val = $this->ReadElement($oChildElement);
+ $value[$oChildElement->getName()] = $val;
+ }
+ break;
+
+ case 'int':
+ case 'integer':
+ $value = (int)$oElement;
+ break;
+
+ case 'string':
+ default:
+ $value = (string)$oElement;
+ }
+
+ return $value;
+ }
+
+ protected function GetAttribute($sAttName, $oElement, $sDefaultValue)
+ {
+ $sRet = $sDefaultValue;
+
+ foreach ($oElement->attributes() as $sKey => $oChildElement) {
+ if ((string)$sKey == $sAttName) {
+ $sRet = (string)$oChildElement;
+ break;
+ }
+ }
+
+ return $sRet;
+ }
+
+ public function Merge(Parameters $oTask)
+ {
+ $this->aData = $this->array_merge_recursive_distinct($this->aData, $oTask->aData);
+ }
+
+ /**
+ * array_merge_recursive does indeed merge arrays, but it converts values with duplicate
+ * keys to arrays rather than overwriting the value in the first array with the duplicate
+ * value in the second array, as array_merge does. I.e., with array_merge_recursive,
+ * this happens (documented behavior):
+ *
+ * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
+ * => array('key' => array('org value', 'new value'));
+ *
+ * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
+ * Matching keys' values in the second array overwrite those in the first array, as is the
+ * case with array_merge, i.e.:
+ *
+ * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
+ * => array('key' => array('new value'));
+ *
+ * Parameters are passed by reference, though only for performance reasons. They're not
+ * altered by this function.
+ *
+ * @param array $array1
+ * @param array $array2
+ *
+ * @return array
+ * @author Daniel
+ * @author Gabriel Sobrinho
+ */
+ protected function array_merge_recursive_distinct(array &$array1, array &$array2)
+ {
+ $merged = $array1;
+
+ foreach ($array2 as $key => &$value) {
+ if (is_array($value) && isset($merged [$key]) && is_array($merged [$key])) {
+ $merged [$key] = $this->array_merge_recursive_distinct($merged [$key], $value);
+ } else {
+ $merged [$key] = $value;
+ }
+ }
+
+ return $merged;
+ }
}
diff --git a/core/polyfill.inc.php b/core/polyfill.inc.php
index dc82eee..b6f6fb5 100644
--- a/core/polyfill.inc.php
+++ b/core/polyfill.inc.php
@@ -5,15 +5,15 @@
* Make this function available even for (PHP 8 < 8.1.0)
*/
if (!function_exists("array_is_list")) {
- function array_is_list(array $array): bool
- {
- $i = 0;
- foreach ($array as $k => $v) {
- if ($k !== $i++) {
- return false;
- }
- }
+ function array_is_list(array $array): bool
+ {
+ $i = 0;
+ foreach ($array as $k => $v) {
+ if ($k !== $i++) {
+ return false;
+ }
+ }
- return true;
- }
+ return true;
+ }
}
diff --git a/core/restclient.class.inc.php b/core/restclient.class.inc.php
index 150841d..630e05d 100644
--- a/core/restclient.class.inc.php
+++ b/core/restclient.class.inc.php
@@ -17,193 +17,193 @@
class RestClient
{
- protected $sVersion;
-
- public function __construct()
- {
- $this->sVersion = '1.0';
- }
-
- public function GetVersion()
- {
- return $this->sVersion;
- }
-
- public function SetVersion($sVersion)
- {
- $this->sVersion = $sVersion;
- }
-
- public function Get($sClass, $keySpec, $sOutputFields = '*', $iLimit = 0)
- {
- $aOperation = [
- 'operation' => 'core/get', // operation code
- 'class' => $sClass,
- 'key' => $keySpec,
- 'output_fields' => $sOutputFields, // list of fields to show in the results (* or a,b,c)
- 'limit' => $iLimit,
- ];
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- public function CheckCredentials($sUser, $sPassword)
- {
- $aOperation = [
- 'operation' => 'core/check_credentials', // operation code
- 'user' => $sUser,
- 'password' => $sPassword,
- ];
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- public function ListOperations()
- {
- $aOperation = [
- 'operation' => 'list_operations', // operation code
- 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
- ];
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- public function Create($sClass, $aFields, $sComment)
- {
- $aOperation = [
- 'operation' => 'core/create', // operation code
- 'class' => $sClass,
- 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
- 'fields' => $aFields,
- 'comment' => $sComment,
- ];
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- public function Update($sClass, $keySpec, $aFields, $sComment)
- {
- $aOperation = [
- 'operation' => 'core/update', // operation code
- 'class' => $sClass,
- 'key' => $keySpec,
- 'fields' => $aFields, // fields to update
- 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
- 'comment' => $sComment,
- ];
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- public function GetRelatedObjects($sClass, $sKey, $sRelation, $bRedundancy = false, $iDepth = 99)
- {
- $aOperation = [
- 'operation' => 'core/get_related', // operation code
- 'class' => $sClass,
- 'key' => $sKey,
- 'relation' => $sRelation,
- 'depth' => $iDepth,
- 'redundancy' => $bRedundancy,
- ];
-
- return self::ExecOperation($aOperation, $this->sVersion);
- }
-
- protected static function ExecOperation($aOperation, $sVersion = '1.0')
- {
- $aData = Utils::GetCredentials();
- $aData['json_data'] = json_encode($aOperation);
- $sLoginform = Utils::GetLoginMode();
- $sUrl = sprintf(
- '%s/webservices/rest.php?login_mode=%s&version=%s',
- Utils::GetConfigurationValue('itop_url', ''),
- $sLoginform,
- $sVersion
- );
- $aHeaders = [];
- $aCurlOptions = Utils::GetCurlOptions();
- $response = Utils::DoPostRequest($sUrl, $aData, '', $aHeaders, $aCurlOptions);
- $aResults = json_decode($response, true);
- if (!$aResults) {
- throw new Exception("rest.php replied: $response");
- }
-
- return $aResults;
- }
-
- public static function GetNewestKnownVersion()
- {
- $sNewestVersion = '1.0';
- $oC = new RestClient();
- $aKnownVersions = ['1.0', '1.1', '1.2', '2.0'];
- foreach ($aKnownVersions as $sVersion) {
- $oC->SetVersion($sVersion);
- $aRet = $oC->ListOperations();
- if ($aRet['code'] == 0) {
- // Supported version
- $sNewestVersion = $sVersion;
- }
- }
-
- return $sNewestVersion;
- }
-
- /**
- * Emulates the behavior of Get('*+') to retrieve all the characteristics
- * of the attribute_list of a given synchro data source
- *
- * @param hash $aSource The definition of 'fields' the Synchro DataSource, as retrieved by Get
- * @param integer $iSourceId The identifier (key) of the Synchro Data Source
- */
- public static function GetFullSynchroDataSource(&$aSource, $iSourceId)
- {
- $bResult = true;
- $aAttributes = [];
- // Optimize the calls to the REST API: one call per finalclass
- foreach ($aSource['attribute_list'] as $aAttr) {
- if (!array_key_exists($aAttr['finalclass'], $aAttributes)) {
- $aAttributes[$aAttr['finalclass']] = [];
- }
- $aAttributes[$aAttr['finalclass']][] = $aAttr['attcode'];
- }
-
- $oRestClient = new RestClient();
- foreach ($aAttributes as $sFinalClass => $aAttCodes) {
- Utils::Log(LOG_DEBUG, "RestClient::Get SELECT $sFinalClass WHERE attcode IN ('".implode("','", $aAttCodes)."') AND sync_source_id = $iSourceId");
- $aResult = $oRestClient->Get($sFinalClass, "SELECT $sFinalClass WHERE attcode IN ('".implode("','", $aAttCodes)."') AND sync_source_id = $iSourceId");
- if ($aResult['code'] != 0) {
- Utils::Log(LOG_ERR, "{$aResult['message']} ({$aResult['code']})");
- $bResult = false;
- } else {
- // Update the SDS Attributes
- foreach ($aSource['attribute_list'] as $idx => $aAttr) {
- foreach ($aResult['objects'] as $aAttDef) {
- if ($aAttDef['fields']['attcode'] == $aAttr['attcode']) {
- $aSource['attribute_list'][$idx] = $aAttDef['fields'];
-
- // fix booleans
- $aSource['attribute_list'][$idx]['reconcile'] = $aAttDef['fields']['reconcile'] ? '1' : '0';
- $aSource['attribute_list'][$idx]['update'] = $aAttDef['fields']['update'] ? '1' : '0';
-
- // read-only (external) fields
- unset($aSource['attribute_list'][$idx]['friendlyname']);
- unset($aSource['attribute_list'][$idx]['sync_source_id']);
- unset($aSource['attribute_list'][$idx]['sync_source_name']);
- unset($aSource['attribute_list'][$idx]['sync_source_id_friendlyname']);
- }
- }
- }
- }
- }
-
- // Don't care about these read-only fields
- unset($aSource['friendlyname']);
- unset($aSource['user_id_friendlyname']);
- unset($aSource['user_id_finalclass_recall']);
- unset($aSource['notify_contact_id_friendlyname']);
- unset($aSource['notify_contact_id_finalclass_recall']);
- unset($aSource['notify_contact_id_obsolescence_flag']);
-
- return $bResult;
- }
+ protected $sVersion;
+
+ public function __construct()
+ {
+ $this->sVersion = '1.0';
+ }
+
+ public function GetVersion()
+ {
+ return $this->sVersion;
+ }
+
+ public function SetVersion($sVersion)
+ {
+ $this->sVersion = $sVersion;
+ }
+
+ public function Get($sClass, $keySpec, $sOutputFields = '*', $iLimit = 0)
+ {
+ $aOperation = [
+ 'operation' => 'core/get', // operation code
+ 'class' => $sClass,
+ 'key' => $keySpec,
+ 'output_fields' => $sOutputFields, // list of fields to show in the results (* or a,b,c)
+ 'limit' => $iLimit,
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ public function CheckCredentials($sUser, $sPassword)
+ {
+ $aOperation = [
+ 'operation' => 'core/check_credentials', // operation code
+ 'user' => $sUser,
+ 'password' => $sPassword,
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ public function ListOperations()
+ {
+ $aOperation = [
+ 'operation' => 'list_operations', // operation code
+ 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ public function Create($sClass, $aFields, $sComment)
+ {
+ $aOperation = [
+ 'operation' => 'core/create', // operation code
+ 'class' => $sClass,
+ 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
+ 'fields' => $aFields,
+ 'comment' => $sComment,
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ public function Update($sClass, $keySpec, $aFields, $sComment)
+ {
+ $aOperation = [
+ 'operation' => 'core/update', // operation code
+ 'class' => $sClass,
+ 'key' => $keySpec,
+ 'fields' => $aFields, // fields to update
+ 'output_fields' => '*', // list of fields to show in the results (* or a,b,c)
+ 'comment' => $sComment,
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ public function GetRelatedObjects($sClass, $sKey, $sRelation, $bRedundancy = false, $iDepth = 99)
+ {
+ $aOperation = [
+ 'operation' => 'core/get_related', // operation code
+ 'class' => $sClass,
+ 'key' => $sKey,
+ 'relation' => $sRelation,
+ 'depth' => $iDepth,
+ 'redundancy' => $bRedundancy,
+ ];
+
+ return self::ExecOperation($aOperation, $this->sVersion);
+ }
+
+ protected static function ExecOperation($aOperation, $sVersion = '1.0')
+ {
+ $aData = Utils::GetCredentials();
+ $aData['json_data'] = json_encode($aOperation);
+ $sLoginform = Utils::GetLoginMode();
+ $sUrl = sprintf(
+ '%s/webservices/rest.php?login_mode=%s&version=%s',
+ Utils::GetConfigurationValue('itop_url', ''),
+ $sLoginform,
+ $sVersion
+ );
+ $aHeaders = [];
+ $aCurlOptions = Utils::GetCurlOptions();
+ $response = Utils::DoPostRequest($sUrl, $aData, '', $aHeaders, $aCurlOptions);
+ $aResults = json_decode($response, true);
+ if (!$aResults) {
+ throw new Exception("rest.php replied: $response");
+ }
+
+ return $aResults;
+ }
+
+ public static function GetNewestKnownVersion()
+ {
+ $sNewestVersion = '1.0';
+ $oC = new RestClient();
+ $aKnownVersions = ['1.0', '1.1', '1.2', '2.0'];
+ foreach ($aKnownVersions as $sVersion) {
+ $oC->SetVersion($sVersion);
+ $aRet = $oC->ListOperations();
+ if ($aRet['code'] == 0) {
+ // Supported version
+ $sNewestVersion = $sVersion;
+ }
+ }
+
+ return $sNewestVersion;
+ }
+
+ /**
+ * Emulates the behavior of Get('*+') to retrieve all the characteristics
+ * of the attribute_list of a given synchro data source
+ *
+ * @param hash $aSource The definition of 'fields' the Synchro DataSource, as retrieved by Get
+ * @param integer $iSourceId The identifier (key) of the Synchro Data Source
+ */
+ public static function GetFullSynchroDataSource(&$aSource, $iSourceId)
+ {
+ $bResult = true;
+ $aAttributes = [];
+ // Optimize the calls to the REST API: one call per finalclass
+ foreach ($aSource['attribute_list'] as $aAttr) {
+ if (!array_key_exists($aAttr['finalclass'], $aAttributes)) {
+ $aAttributes[$aAttr['finalclass']] = [];
+ }
+ $aAttributes[$aAttr['finalclass']][] = $aAttr['attcode'];
+ }
+
+ $oRestClient = new RestClient();
+ foreach ($aAttributes as $sFinalClass => $aAttCodes) {
+ Utils::Log(LOG_DEBUG, "RestClient::Get SELECT $sFinalClass WHERE attcode IN ('".implode("','", $aAttCodes)."') AND sync_source_id = $iSourceId");
+ $aResult = $oRestClient->Get($sFinalClass, "SELECT $sFinalClass WHERE attcode IN ('".implode("','", $aAttCodes)."') AND sync_source_id = $iSourceId");
+ if ($aResult['code'] != 0) {
+ Utils::Log(LOG_ERR, "{$aResult['message']} ({$aResult['code']})");
+ $bResult = false;
+ } else {
+ // Update the SDS Attributes
+ foreach ($aSource['attribute_list'] as $idx => $aAttr) {
+ foreach ($aResult['objects'] as $aAttDef) {
+ if ($aAttDef['fields']['attcode'] == $aAttr['attcode']) {
+ $aSource['attribute_list'][$idx] = $aAttDef['fields'];
+
+ // fix booleans
+ $aSource['attribute_list'][$idx]['reconcile'] = $aAttDef['fields']['reconcile'] ? '1' : '0';
+ $aSource['attribute_list'][$idx]['update'] = $aAttDef['fields']['update'] ? '1' : '0';
+
+ // read-only (external) fields
+ unset($aSource['attribute_list'][$idx]['friendlyname']);
+ unset($aSource['attribute_list'][$idx]['sync_source_id']);
+ unset($aSource['attribute_list'][$idx]['sync_source_name']);
+ unset($aSource['attribute_list'][$idx]['sync_source_id_friendlyname']);
+ }
+ }
+ }
+ }
+ }
+
+ // Don't care about these read-only fields
+ unset($aSource['friendlyname']);
+ unset($aSource['user_id_friendlyname']);
+ unset($aSource['user_id_finalclass_recall']);
+ unset($aSource['notify_contact_id_friendlyname']);
+ unset($aSource['notify_contact_id_finalclass_recall']);
+ unset($aSource['notify_contact_id_obsolescence_flag']);
+
+ return $bResult;
+ }
}
diff --git a/core/sqlcollector.class.inc.php b/core/sqlcollector.class.inc.php
index 5470be2..18dc9a8 100644
--- a/core/sqlcollector.class.inc.php
+++ b/core/sqlcollector.class.inc.php
@@ -32,165 +32,165 @@
*/
abstract class SQLCollector extends Collector
{
- protected $oDB;
- protected $oStatement;
- protected $idx;
- protected $sQuery;
-
- /**
- * Initalization
- */
- public function __construct()
- {
- parent::__construct();
- $this->oDB = null;
- $this->oStatement = null;
- }
-
- /**
- * Runs the configured query to start fetching the data from the database
- *
- * @see Collector::Prepare()
- */
- public function Prepare()
- {
- $bRet = parent::Prepare();
- if (!$bRet) {
- return false;
- }
-
- $bRet = $this->Connect(); // Establish the connection to the database
- if (!$bRet) {
- return false;
- }
-
- // Read the SQL query from the configuration
- $this->sQuery = Utils::GetConfigurationValue(get_class($this)."_query", '');
- if ($this->sQuery == '') {
- // Try all lowercase
- $this->sQuery = Utils::GetConfigurationValue(strtolower(get_class($this))."_query", '');
- }
- if ($this->sQuery == '') {
- // No query at all !!
- Utils::Log(LOG_ERR, "[".get_class($this)."] no SQL query configured! Cannot collect data. The query was expected to be configured as '".strtolower(get_class($this))."_query' in the configuration file.");
-
- return false;
- }
-
- $this->oStatement = $this->oDB->prepare($this->sQuery);
- if ($this->oStatement === false) {
- $aInfo = $this->oDB->errorInfo();
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to prepare the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
-
- return false;
- }
-
- $this->oStatement->execute();
- if ($this->oStatement->errorCode() !== '00000') {
- $aInfo = $this->oStatement->errorInfo();
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to execute the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
-
- return false;
- }
-
- $this->idx = 0;
-
- return true;
- }
-
- /**
- * Establish the connection to the database, based on the configuration parameters.
- * By default all collectors derived from SQLCollector will share the same connection
- * parameters (same DB server, login, DB name...). If you don't want this behavior,
- * overload this method in your connector.
- */
- protected function Connect()
- {
- $aAvailableDrivers = PDO::getAvailableDrivers();
-
- Utils::Log(LOG_DEBUG, "Available PDO drivers: ".implode(', ', $aAvailableDrivers));
-
- $sEngine = Utils::GetConfigurationValue('sql_engine', 'mysql');
- if (!in_array($sEngine, $aAvailableDrivers)) {
- Utils::Log(LOG_ERR, "The requested PDO driver: '$sEngine' is not installed on this system. Available PDO drivers: ".implode(', ', $aAvailableDrivers));
- }
- $sHost = Utils::GetConfigurationValue('sql_host', 'localhost');
- $sDatabase = Utils::GetConfigurationValue('sql_database', '');
- $sLogin = Utils::GetConfigurationValue('sql_login', 'root');
- $sPassword = Utils::GetConfigurationValue('sql_password', '');
-
- $sConnectionStringFormat = Utils::GetConfigurationValue('sql_connection_string', '%1$s:dbname=%2$s;host=%3$s');
- $sConnectionString = sprintf($sConnectionStringFormat, $sEngine, $sDatabase, $sHost);
-
- Utils::Log(LOG_DEBUG, "[".get_class($this)."] Connection string: '$sConnectionString'");
-
- try {
- $this->oDB = new PDO($sConnectionString, $sLogin, $sPassword);
- } catch (PDOException $e) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] Database connection failed: ".$e->getMessage());
- $this->oDB = null;
-
- return false;
- }
-
- return true;
- }
-
- /**
- * Fetch one row of data from the database
- * The first row is used to check if the columns of the result match the expected "fields"
- *
- * @see Collector::Fetch()
- */
- public function Fetch()
- {
- if ($aData = $this->oStatement->fetch(PDO::FETCH_ASSOC)) {
-
- foreach ($this->aSkippedAttributes as $sCode) {
- unset($aData[$sCode]);
- }
-
- if ($this->idx == 0) {
- $this->CheckColumns($aData, [], 'SQL query');
- }
- $this->idx++;
-
- return $aData;
- }
-
- return false;
- }
-
- /**
- * Determine if a given attribute is allowed to be missing in the data datamodel.
- *
- * The implementation is based on a predefined configuration parameter named from the
- * class of the collector (all lowercase) with _ignored_attributes appended.
- *
- * Example: here is the configuration to "ignore" the attribute 'location_id' for the class MySQLCollector:
- *
- * location_id
- *
- *
- * @param string $sAttCode
- *
- * @return boolean True if the attribute can be skipped, false otherwise
- */
- public function AttributeIsOptional($sAttCode)
- {
- $aIgnoredAttributes = Utils::GetConfigurationValue(get_class($this)."_ignored_attributes", null);
- if ($aIgnoredAttributes === null) {
- // Try all lowercase
- $aIgnoredAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_ignored_attributes", null);
- }
- if (is_array($aIgnoredAttributes)) {
- if (in_array($sAttCode, $aIgnoredAttributes)) {
- return true;
- }
- }
-
- return parent::AttributeIsOptional($sAttCode);
- }
+ protected $oDB;
+ protected $oStatement;
+ protected $idx;
+ protected $sQuery;
+
+ /**
+ * Initalization
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ $this->oDB = null;
+ $this->oStatement = null;
+ }
+
+ /**
+ * Runs the configured query to start fetching the data from the database
+ *
+ * @see Collector::Prepare()
+ */
+ public function Prepare()
+ {
+ $bRet = parent::Prepare();
+ if (!$bRet) {
+ return false;
+ }
+
+ $bRet = $this->Connect(); // Establish the connection to the database
+ if (!$bRet) {
+ return false;
+ }
+
+ // Read the SQL query from the configuration
+ $this->sQuery = Utils::GetConfigurationValue(get_class($this)."_query", '');
+ if ($this->sQuery == '') {
+ // Try all lowercase
+ $this->sQuery = Utils::GetConfigurationValue(strtolower(get_class($this))."_query", '');
+ }
+ if ($this->sQuery == '') {
+ // No query at all !!
+ Utils::Log(LOG_ERR, "[".get_class($this)."] no SQL query configured! Cannot collect data. The query was expected to be configured as '".strtolower(get_class($this))."_query' in the configuration file.");
+
+ return false;
+ }
+
+ $this->oStatement = $this->oDB->prepare($this->sQuery);
+ if ($this->oStatement === false) {
+ $aInfo = $this->oDB->errorInfo();
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to prepare the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
+
+ return false;
+ }
+
+ $this->oStatement->execute();
+ if ($this->oStatement->errorCode() !== '00000') {
+ $aInfo = $this->oStatement->errorInfo();
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to execute the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
+
+ return false;
+ }
+
+ $this->idx = 0;
+
+ return true;
+ }
+
+ /**
+ * Establish the connection to the database, based on the configuration parameters.
+ * By default all collectors derived from SQLCollector will share the same connection
+ * parameters (same DB server, login, DB name...). If you don't want this behavior,
+ * overload this method in your connector.
+ */
+ protected function Connect()
+ {
+ $aAvailableDrivers = PDO::getAvailableDrivers();
+
+ Utils::Log(LOG_DEBUG, "Available PDO drivers: ".implode(', ', $aAvailableDrivers));
+
+ $sEngine = Utils::GetConfigurationValue('sql_engine', 'mysql');
+ if (!in_array($sEngine, $aAvailableDrivers)) {
+ Utils::Log(LOG_ERR, "The requested PDO driver: '$sEngine' is not installed on this system. Available PDO drivers: ".implode(', ', $aAvailableDrivers));
+ }
+ $sHost = Utils::GetConfigurationValue('sql_host', 'localhost');
+ $sDatabase = Utils::GetConfigurationValue('sql_database', '');
+ $sLogin = Utils::GetConfigurationValue('sql_login', 'root');
+ $sPassword = Utils::GetConfigurationValue('sql_password', '');
+
+ $sConnectionStringFormat = Utils::GetConfigurationValue('sql_connection_string', '%1$s:dbname=%2$s;host=%3$s');
+ $sConnectionString = sprintf($sConnectionStringFormat, $sEngine, $sDatabase, $sHost);
+
+ Utils::Log(LOG_DEBUG, "[".get_class($this)."] Connection string: '$sConnectionString'");
+
+ try {
+ $this->oDB = new PDO($sConnectionString, $sLogin, $sPassword);
+ } catch (PDOException $e) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Database connection failed: ".$e->getMessage());
+ $this->oDB = null;
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Fetch one row of data from the database
+ * The first row is used to check if the columns of the result match the expected "fields"
+ *
+ * @see Collector::Fetch()
+ */
+ public function Fetch()
+ {
+ if ($aData = $this->oStatement->fetch(PDO::FETCH_ASSOC)) {
+
+ foreach ($this->aSkippedAttributes as $sCode) {
+ unset($aData[$sCode]);
+ }
+
+ if ($this->idx == 0) {
+ $this->CheckColumns($aData, [], 'SQL query');
+ }
+ $this->idx++;
+
+ return $aData;
+ }
+
+ return false;
+ }
+
+ /**
+ * Determine if a given attribute is allowed to be missing in the data datamodel.
+ *
+ * The implementation is based on a predefined configuration parameter named from the
+ * class of the collector (all lowercase) with _ignored_attributes appended.
+ *
+ * Example: here is the configuration to "ignore" the attribute 'location_id' for the class MySQLCollector:
+ *
+ * location_id
+ *
+ *
+ * @param string $sAttCode
+ *
+ * @return boolean True if the attribute can be skipped, false otherwise
+ */
+ public function AttributeIsOptional($sAttCode)
+ {
+ $aIgnoredAttributes = Utils::GetConfigurationValue(get_class($this)."_ignored_attributes", null);
+ if ($aIgnoredAttributes === null) {
+ // Try all lowercase
+ $aIgnoredAttributes = Utils::GetConfigurationValue(strtolower(get_class($this))."_ignored_attributes", null);
+ }
+ if (is_array($aIgnoredAttributes)) {
+ if (in_array($sAttCode, $aIgnoredAttributes)) {
+ return true;
+ }
+ }
+
+ return parent::AttributeIsOptional($sAttCode);
+ }
}
/**
@@ -208,41 +208,41 @@ public function AttributeIsOptional($sAttCode)
*/
abstract class MySQLCollector extends SQLCollector
{
- /**
- * Establish the connection to the database, based on the configuration parameters.
- * By default all collectors derived from SQLCollector will share the same connection
- * parameters (same DB server, login, DB name...).
- * Moreover, forces the connection to use utf8 using the SET NAMES SQL command.
- * If you don't want this behavior, overload this method in your connector.
- */
- protected function Connect()
- {
- $bRet = parent::Connect();
- if ($bRet) {
- try {
- $this->oStatement = $this->oDB->prepare("SET NAMES 'utf8'");
- if ($this->oStatement === false) {
- $aInfo = $this->oDB->errorInfo();
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to prepare the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
-
- return false;
- }
-
- $bRet = $this->oStatement->execute();
- if ($this->oStatement->errorCode() !== '00000') {
- $aInfo = $this->oStatement->errorInfo();
- Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to execute the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
-
- return false;
- }
- } catch (PDOException $e) {
- Utils::Log(LOG_ERR, "[".get_class($this)."] SQL query: \"SET NAMES 'utf8'\" failed: ".$e->getMessage());
- $this->oDB = null;
-
- return false;
- }
- }
-
- return $bRet;
- }
+ /**
+ * Establish the connection to the database, based on the configuration parameters.
+ * By default all collectors derived from SQLCollector will share the same connection
+ * parameters (same DB server, login, DB name...).
+ * Moreover, forces the connection to use utf8 using the SET NAMES SQL command.
+ * If you don't want this behavior, overload this method in your connector.
+ */
+ protected function Connect()
+ {
+ $bRet = parent::Connect();
+ if ($bRet) {
+ try {
+ $this->oStatement = $this->oDB->prepare("SET NAMES 'utf8'");
+ if ($this->oStatement === false) {
+ $aInfo = $this->oDB->errorInfo();
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to prepare the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
+
+ return false;
+ }
+
+ $bRet = $this->oStatement->execute();
+ if ($this->oStatement->errorCode() !== '00000') {
+ $aInfo = $this->oStatement->errorInfo();
+ Utils::Log(LOG_ERR, "[".get_class($this)."] Failed to execute the query: '$this->sQuery'. Reason: ".$aInfo[0].', '.$aInfo[2]);
+
+ return false;
+ }
+ } catch (PDOException $e) {
+ Utils::Log(LOG_ERR, "[".get_class($this)."] SQL query: \"SET NAMES 'utf8'\" failed: ".$e->getMessage());
+ $this->oDB = null;
+
+ return false;
+ }
+ }
+
+ return $bRet;
+ }
}
diff --git a/core/utils.class.inc.php b/core/utils.class.inc.php
index 1b066d0..441c1f4 100644
--- a/core/utils.class.inc.php
+++ b/core/utils.class.inc.php
@@ -23,731 +23,731 @@
class Utils
{
- public static $iConsoleLogLevel = LOG_INFO;
- public static $iSyslogLogLevel = LOG_NONE;
- public static $iEventIssueLogLevel = LOG_NONE;
- public static $sProjectName = "";
- public static $sStep = "";
- public static $oCollector = "";
- protected static $oConfig = null;
- protected static $aConfigFiles = [];
- protected static $iMockLogLevel = LOG_ERR;
-
- protected static $oMockedLogger;
-
- /**
- * @since 1.3.0 N°6012
- */
- protected static $oMockedDoPostRequestService;
-
- /**
- * @var string Keeps track of the latest date the datamodel has been installed/updated
- * (in order to check which modules were installed with it)
- */
- protected static $sLastInstallDate;
-
- public static function SetProjectName($sProjectName)
- {
- if ($sProjectName != null) {
- self::$sProjectName = $sProjectName;
- }
- }
-
- public static function SetCollector($oCollector, $sStep = "")
- {
- self::$oCollector = $oCollector;
- self::$sStep = $sStep;
- }
-
- public static function ReadParameter($sParamName, $defaultValue)
- {
- global $argv;
-
- $retValue = $defaultValue;
- if (is_array($argv)) {
- foreach ($argv as $iArg => $sArg) {
- if (preg_match('/^--'.$sParamName.'=(.*)$/', $sArg, $aMatches)) {
- $retValue = $aMatches[1];
- }
- }
- }
-
- return $retValue;
- }
-
- public static function ReadBooleanParameter($sParamName, $defaultValue)
- {
- global $argv;
-
- $retValue = $defaultValue;
- if (is_array($argv)) {
- foreach ($argv as $iArg => $sArg) {
- if (preg_match('/^--'.$sParamName.'$/', $sArg, $aMatches)) {
- $retValue = true;
- } elseif (preg_match('/^--'.$sParamName.'=(.*)$/', $sArg, $aMatches)) {
- $retValue = ($aMatches[1] != 0);
- }
- }
- }
-
- return $retValue;
- }
-
- public static function CheckParameters($aOptionalParams)
- {
- global $argv;
-
- $aUnknownParams = [];
- if (is_array($argv)) {
- foreach ($argv as $iArg => $sArg) {
- if ($iArg == 0) {
- continue;
- } // Skip program name
- if (preg_match('/^--([A-Za-z0-9_]+)$/', $sArg, $aMatches)) {
- // Looks like a boolean parameter
- if (!array_key_exists($aMatches[1], $aOptionalParams) || ($aOptionalParams[$aMatches[1]] != 'boolean')) {
- $aUnknownParams[] = $sArg;
- }
- } elseif (preg_match('/^--([A-Za-z0-9_]+)=(.*)$/', $sArg, $aMatches)) {
- // Looks like a regular parameter
- if (!array_key_exists($aMatches[1], $aOptionalParams) || ($aOptionalParams[$aMatches[1]] == 'boolean')) {
- $aUnknownParams[] = $sArg;
- }
- } else {
- $aUnknownParams[] = $sArg;
- }
- }
- }
-
- return $aUnknownParams;
- }
-
- /**
- * Init the console log level.
- *
- * Defaults to LOG_INFO if `console_log_level` is not configured
- * Can be overridden by `console_log_level` commandline argument.
- *
- * @throws Exception
- */
- public static function InitConsoleLogLevel()
- {
- $iDefaultConsoleLogLevel = static::GetConfigurationValue('console_log_level', LOG_INFO);
- static::$iConsoleLogLevel = static::ReadParameter('console_log_level', $iDefaultConsoleLogLevel);
- }
-
- /**
- * Logs a message to the centralized log for the application, with the given priority
- *
- * @param int $iPriority Use the LOG_* constants for priority e.g. LOG_WARNING, LOG_INFO, LOG_ERR... (see:
- * www.php.net/manual/en/function.syslog.php)
- * @param string $sMessage The message to log
- *
- * @return void
- * @throws \Exception
- */
- public static function Log($iPriority, $sMessage)
- {
- //testing only LOG_ERR
- if (self::$oMockedLogger) {
- if ($iPriority <= self::$iMockLogLevel) {
- var_dump($sMessage);
- self::$oMockedLogger->Log($iPriority, $sMessage);
- }
- }
-
- switch ($iPriority) {
- case LOG_EMERG:
- $sPrio = 'Emergency';
- break;
-
- case LOG_ALERT:
- $sPrio = 'Alert';
- break;
- case LOG_CRIT:
- $sPrio = 'Critical Error';
- break;
-
- case LOG_ERR:
- $sPrio = 'Error';
- break;
-
- case LOG_WARNING:
- $sPrio = 'Warning';
- break;
-
- case LOG_NOTICE:
- $sPrio = 'Notice';
- break;
-
- case LOG_INFO:
- $sPrio = 'Info';
- break;
-
- case LOG_DEBUG:
- $sPrio = 'Debug';
- break;
-
- default:
- $sPrio = 'Critical Error';
- }
-
- if ($iPriority <= self::$iConsoleLogLevel) {
- $log_date_format = self::GetConfigurationValue("console_log_dateformat", "[Y-m-d H:i:s]");
- $txt = date($log_date_format)."\t[".$sPrio."]\t".$sMessage."\n";
- echo $txt;
- }
-
- if ($iPriority <= self::$iSyslogLogLevel) {
- openlog('iTop Data Collector', LOG_PID, LOG_USER);
- syslog($iPriority, $sMessage);
- closelog();
- }
-
- if ($iPriority <= self::$iEventIssueLogLevel) {
- Utils::CreateEventIssue($sMessage);
- }
- }
-
- /**
- * @param bool $bResult
- * @param string $sErrorMessage
- */
- private static function CreateEventIssue($sMessage)
- {
- $sProjectName = self::$sProjectName;
- $sCollectorName = (self::$oCollector == null) ? "" : get_class(self::$oCollector);
- $sStep = self::$sStep;
-
- $aFields = [
- "message" => "$sMessage",
- "userinfo" => "Collector",
- "issue" => "$sStep-$sCollectorName",
- "impact" => "$sProjectName",
- ];
-
- $oClient = new RestClient();
- $oClient->Create("EventIssue", $aFields, "create event issue from collector $sCollectorName execution.");
- }
-
- public static function MockLog($oMockedLogger, $iMockLogLevel = LOG_ERR)
- {
- self::$oMockedLogger = $oMockedLogger;
- self::$iMockLogLevel = $iMockLogLevel;
- }
-
- /**
- * @param DoPostRequestService|null $oMockedDoPostRequestService
- * @since 1.3.0 N°6012
- * @return void
- */
- public static function MockDoPostRequestService($oMockedDoPostRequestService)
- {
- self::$oMockedDoPostRequestService = $oMockedDoPostRequestService;
- }
-
- /**
- * Load the configuration from the various XML configuration files
- *
- * @return Parameters
- * @throws Exception
- */
- public static function LoadConfig()
- {
- $sCustomConfigFile = Utils::ReadParameter('config_file', null);
-
- self::$aConfigFiles[] = CONF_DIR.'params.distrib.xml';
- self::$oConfig = new Parameters(CONF_DIR.'params.distrib.xml');
- if (file_exists(APPROOT.'collectors/params.distrib.xml')) {
- self::MergeConfFile(APPROOT.'collectors/params.distrib.xml');
- }
- if (file_exists(APPROOT.'collectors/extensions/params.distrib.xml')) {
- self::MergeConfFile(APPROOT.'collectors/extensions/params.distrib.xml');
- }
- if ($sCustomConfigFile !== null) {
- // A custom config file was supplied on the command line
- if (file_exists($sCustomConfigFile)) {
- self::MergeConfFile($sCustomConfigFile);
- } else {
- throw new Exception("The specified configuration file '$sCustomConfigFile' does not exist.");
- }
- } elseif (file_exists(CONF_DIR.'params.local.xml')) {
- self::MergeConfFile(CONF_DIR.'params.local.xml');
- }
-
- return self::$oConfig;
- }
-
- private static function MergeConfFile($sFilePath)
- {
- self::$aConfigFiles[] = $sFilePath;
- $oLocalConfig = new Parameters($sFilePath);
- self::$oConfig->Merge($oLocalConfig);
- }
-
- /**
- * Get the value of a configuration parameter
- *
- * @param string $sCode
- * @param mixed $defaultValue
- *
- * @return mixed
- * @throws Exception
- */
- public static function GetConfigurationValue($sCode, $defaultValue = '')
- {
- if (self::$oConfig == null) {
- self::LoadConfig();
- }
-
- $value = self::$oConfig->Get($sCode, $defaultValue);
- $value = self::Substitute($value);
-
- return $value;
- }
-
- /**
- * @since 1.3.0 N°6012
- */
- public static function GetCredentials(): array
- {
- $sToken = Utils::GetConfigurationValue('itop_token', '');
- if (strlen($sToken) > 0) {
- return [
- 'auth_token' => $sToken
- ];
- }
-
- return [
- 'auth_user' => Utils::GetConfigurationValue('itop_login', ''),
- 'auth_pwd' => Utils::GetConfigurationValue('itop_password', ''),
- ];
- }
-
- /**
- * @since 1.3.0 N°6012
- */
- public static function GetLoginMode(): string
- {
- $sLoginform = Utils::GetConfigurationValue('itop_login_mode', '');
- if (strlen($sLoginform) > 0) {
- return $sLoginform;
- }
-
- $sToken = Utils::GetConfigurationValue('itop_token', '');
- if (strlen($sToken) > 0) {
- return 'token';
- }
-
- return 'form';
- }
-
- /**
- * Dump information about the configuration (value of the parameters)
- *
- * @return string
- * @throws Exception
- */
- public static function DumpConfig()
- {
- if (self::$oConfig == null) {
- self::LoadConfig();
- }
-
- return self::$oConfig->Dump();
- }
-
- /**
- * Get the ordered list of configuration files loaded
- *
- * @return string
- * @throws Exception
- */
- public static function GetConfigFiles()
- {
- if (self::$oConfig == null) {
- self::LoadConfig();
- }
-
- return self::$aConfigFiles;
- }
-
- protected static function Substitute($value)
- {
- if (is_array($value)) {
- // Recursiverly process each entry
- foreach ($value as $key => $val) {
- $value[$key] = self::Substitute($val);
- }
- } elseif (is_string($value)) {
- preg_match_all('/\$([A-Za-z0-9-_]+)\$/', $value, $aMatches);
- $aReplacements = [];
- if (count($aMatches) > 0) {
- foreach ($aMatches[1] as $sSubCode) {
- $aReplacements['$'.$sSubCode.'$'] = self::GetConfigurationValue($sSubCode, '#ERROR_UNDEFINED_PLACEHOLDER_'.$sSubCode.'#');
- }
- $value = str_replace(array_keys($aReplacements), $aReplacements, $value);
- }
- } else {
- // Do nothing, return as-is
- }
-
- return $value;
- }
-
- /**
- * Return the (valid) location where to store some temporary data
- * Throws an exception if the directory specified in the 'data_path' configuration does not exist and cannot be created
- *
- * @param string $sFileName
- *
- * @return string
- * @throws Exception
- */
- public static function GetDataFilePath($sFileName)
- {
- $sPath = static::GetConfigurationValue('data_path', '%APPROOT%/data/');
- $sPath = str_replace('%APPROOT%', APPROOT, $sPath); // substitute the %APPROOT% placeholder with its actual value
- $sPath = rtrim($sPath, '/').'/'; // Make that the path ends with exactly one /
- if (!file_exists($sPath)) {
- if (!mkdir($sPath, 0700, true)) {
- throw new Exception("Failed to create data_path: '$sPath'. Either create the directory yourself or make sure that the script has enough rights to create it.");
- }
- }
-
- return $sPath.basename($sFileName);
- }
-
- /**
- * Helper to execute an HTTP POST request
- * Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl
- * originaly named after do_post_request
- * Does not require cUrl but requires openssl for performing https POSTs.
- *
- * @param string $sUrl The URL to POST the data to
- * @param hash $aData The data to POST as an array('param_name' => value)
- * @param string $sOptionnalHeaders Additional HTTP headers as a string with newlines between headers
- * @param hash $aResponseHeaders An array to be filled with reponse headers: WARNING: the actual content of the array depends on the library used: cURL or fopen, test with both !! See: http://fr.php.net/manual/en/function.curl-getinfo.php
- * @param hash $aCurlOptions An (optional) array of options to pass to curl_init. The format is 'option_code' => 'value'. These values have precedence over the default ones
- *
- * @return string The result of the POST request
- * @throws Exception
- */
- public static function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = [])
- {
- if (self::$oMockedDoPostRequestService) {
- return self::$oMockedDoPostRequestService->DoPostRequest($sUrl, $aData, $sOptionnalHeaders, $aResponseHeaders, $aCurlOptions);
- }
-
- // $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request.
-
- if (function_exists('curl_init')) {
- // If cURL is available, let's use it, since it provides a greater control over the various HTTP/SSL options
- // For instance fopen does not allow to work around the bug: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112
- // by setting the SSLVERSION to 3 as done below.
- $aHeaders = explode("\n", $sOptionnalHeaders);
- // N°3267 - Webservices: Fix optional headers not being taken into account
- // See https://www.php.net/curl_setopt CURLOPT_HTTPHEADER
- $aHTTPHeaders = [];
- foreach ($aHeaders as $sHeaderString) {
- $aHTTPHeaders[] = trim($sHeaderString);
- }
- // Default options, can be overloaded/extended with the 4th parameter of this method, see above $aCurlOptions
- $aOptions = [
- CURLOPT_RETURNTRANSFER => true, // return the content of the request
- CURLOPT_HEADER => false, // don't return the headers in the output
- CURLOPT_FOLLOWLOCATION => true, // follow redirects
- CURLOPT_ENCODING => "", // handle all encodings
- CURLOPT_USERAGENT => "spider", // who am i
- CURLOPT_AUTOREFERER => true, // set referer on redirect
- CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect
- CURLOPT_TIMEOUT => 120, // timeout on response
- CURLOPT_MAXREDIRS => 10, // stop after 10 redirects
- CURLOPT_SSL_VERIFYHOST => 0, // Disabled SSL Cert checks
- CURLOPT_SSL_VERIFYPEER => 0, // Disabled SSL Cert checks
- // SSLV3 (CURL_SSLVERSION_SSLv3 = 3) is now considered as obsolete/dangerous: http://disablessl3.com/#why
- // but it used to be a MUST to prevent a strange SSL error: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112
- // CURLOPT_SSLVERSION => 3,
- CURLOPT_POST => count($aData),
- CURLOPT_POSTFIELDS => http_build_query($aData),
- CURLOPT_HTTPHEADER => $aHTTPHeaders,
- ];
-
- $aAllOptions = $aCurlOptions + $aOptions;
- $ch = curl_init($sUrl);
- curl_setopt_array($ch, $aAllOptions);
- $response = curl_exec($ch);
- $iErr = curl_errno($ch);
- $sErrMsg = curl_error($ch);
- $aHeaders = curl_getinfo($ch);
- if ($iErr !== 0) {
- throw new IOException("Problem opening URL: $sUrl"
- .PHP_EOL." error msg: $sErrMsg"
- .PHP_EOL." curl_init error code: $iErr (cf https://www.php.net/manual/en/function.curl-errno.php)");
- }
- if (is_array($aResponseHeaders)) {
- $aHeaders = curl_getinfo($ch);
- foreach ($aHeaders as $sCode => $sValue) {
- $sName = str_replace(' ', '-', ucwords(str_replace('_', ' ', $sCode))); // Transform "content_type" into "Content-Type"
- $aResponseHeaders[$sName] = $sValue;
- }
- }
- curl_close($ch);
- } else {
- // cURL is not available let's try with streams and fopen...
-
- $sData = http_build_query($aData);
- $aParams = [
- 'http' => [
- 'method' => 'POST',
- 'content' => $sData,
- 'header' => "Content-type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($sData)."\r\n",
- ],
- ];
- if ($sOptionnalHeaders !== null) {
- $aParams['http']['header'] .= $sOptionnalHeaders;
- }
- $ctx = stream_context_create($aParams);
-
- $fp = @fopen($sUrl, 'rb', false, $ctx);
- if (!$fp) {
- $error_arr = error_get_last();
- if (is_array($error_arr)) {
- throw new IOException("Wrong URL: $sUrl, Error: ".json_encode($error_arr));
- } elseif ((strtolower(substr($sUrl, 0, 5)) == 'https') && !extension_loaded('openssl')) {
- throw new IOException("Cannot connect to $sUrl: missing module 'openssl'");
- } else {
- throw new IOException("Wrong URL: $sUrl");
- }
- }
- $response = @stream_get_contents($fp);
- if ($response === false) {
- throw new IOException("Problem reading data from $sUrl, " . error_get_last());
- }
- if (is_array($aResponseHeaders)) {
- $aMeta = stream_get_meta_data($fp);
- $aHeaders = $aMeta['wrapper_data'];
- foreach ($aHeaders as $sHeaderString) {
- if (preg_match('/^([^:]+): (.+)$/', $sHeaderString, $aMatches)) {
- $aResponseHeaders[$aMatches[1]] = trim($aMatches[2]);
- }
- }
- }
- }
-
- return $response;
- }
-
- /**
- * Pretty print a JSON formatted string. Copied/pasted from http://stackoverflow.com/questions/6054033/pretty-printing-json-with-php
- *
- * @deprecated 1.3.0 use `json_encode($value, JSON_PRETTY_PRINT);` instead (PHP 5.4.0 required)
- *
- * @param string $json A JSON formatted object definition
- *
- * @return string The nicely formatted JSON definition
- */
- public static function JSONPrettyPrint($json)
- {
- Utils::Log(LOG_NOTICE, 'Use of deprecated method '.__METHOD__);
-
- $result = '';
- $level = 0;
- $in_quotes = false;
- $in_escape = false;
- $ends_line_level = null;
- $json_length = strlen($json);
-
- for ($i = 0; $i < $json_length; $i++) {
- $char = $json[$i];
- $new_line_level = null;
- $post = "";
- if ($ends_line_level !== null) {
- $new_line_level = $ends_line_level;
- $ends_line_level = null;
- }
- if ($in_escape) {
- $in_escape = false;
- } elseif ($char === '"') {
- $in_quotes = !$in_quotes;
- } elseif (!$in_quotes) {
- switch ($char) {
- case '}':
- case ']':
- $level--;
- $ends_line_level = null;
- $new_line_level = $level;
- break;
-
- case '{':
- case '[':
- $level++;
- // no break
- case ',':
- $ends_line_level = $level;
- break;
-
- case ':':
- $post = " ";
- break;
-
- case " ":
- case "\t":
- case "\n":
- case "\r":
- $char = "";
- $ends_line_level = $new_line_level;
- $new_line_level = null;
- break;
- }
- } elseif ($char === '\\') {
- $in_escape = true;
- }
- if ($new_line_level !== null) {
- $result .= "\n".str_repeat("\t", $new_line_level);
- }
- $result .= $char.$post;
- }
-
- return $result;
- }
-
- /**
- * Executes a command and returns an array with exit code, stdout and stderr content
- *
- * @param string $cmd - Command to execute
- *
- * @return false|string
- * @throws \Exception
- */
- public static function Exec($sCmd)
- {
- $iBeginTime = time();
- $sWorkDir = APPROOT;
- $aDescriptorSpec = [
- 0 => ["pipe", "r"], // stdin
- 1 => ["pipe", "w"], // stdout
- 2 => ["pipe", "w"], // stderr
- ];
- Utils::Log(LOG_INFO, "Command: $sCmd. Workdir: $sWorkDir");
- $rProcess = proc_open($sCmd, $aDescriptorSpec, $aPipes, $sWorkDir, null);
-
- $sStdOut = stream_get_contents($aPipes[1]);
- fclose($aPipes[1]);
-
- $sStdErr = stream_get_contents($aPipes[2]);
- fclose($aPipes[2]);
-
- $iCode = proc_close($rProcess);
-
- $iElapsed = time() - $iBeginTime;
- if (0 === $iCode) {
- Utils::Log(LOG_INFO, "elapsed:{$iElapsed}s output: $sStdOut");
-
- return $sStdOut;
- } else {
- throw new Exception("Command failed : $sCmd \n\t\t=== with status:$iCode \n\t\t=== stderr:$sStdErr \n\t\t=== stdout: $sStdOut");
- }
- }
-
- /**
- * @since 1.3.0
- */
- public static function GetCurlOptions(int $iCurrentTimeOut = -1): array
- {
- $aRawCurlOptions = Utils::GetConfigurationValue('curl_options', [CURLOPT_SSLVERSION => CURL_SSLVERSION_SSLv3]);
- return self::ComputeCurlOptions($aRawCurlOptions, $iCurrentTimeOut);
- }
-
- /**
- * @since 1.3.0
- */
- public static function ComputeCurlOptions(array $aRawCurlOptions, int $iCurrentTimeOut): array
- {
- $aCurlOptions = [];
- foreach ($aRawCurlOptions as $key => $value) {
- // Convert strings like 'CURLOPT_SSLVERSION' to the value of the corresponding define i.e CURLOPT_SSLVERSION = 32 !
- $iKey = (!is_numeric($key)) ? constant((string)$key) : (int)$key;
- $aCurlOptions[$iKey] = (!is_numeric($value) && defined($value)) ? constant($value) : $value;
- }
-
- if ($iCurrentTimeOut !== -1) {
- $aCurlOptions[CURLOPT_CONNECTTIMEOUT] = $iCurrentTimeOut;
- $aCurlOptions[CURLOPT_TIMEOUT] = $iCurrentTimeOut;
- }
-
- return $aCurlOptions;
- }
-
- /**
- * Check if the given module is installed in iTop.
- * Mind that this assumes the `ModuleInstallation` class is ordered by descending installation date
- *
- * @param string $sModuleId Name of the module to be found, optionally included version (e.g. "some-module" or "some-module/1.2.3")
- * @param bool $bRequired Whether to throw exceptions when module not found
- * @param RestClient|null $oClient
- * @return bool True when the given module is installed, false otherwise
- * @throws Exception When the module is required but could not be found
- */
- public static function CheckModuleInstallation(string $sModuleId, bool $bRequired = false, RestClient $oClient = null): bool
- {
- if (!isset($oClient)) {
- $oClient = new RestClient();
- }
-
- $sName = '';
- $sOperator = '';
- if (preg_match('/^([^\/]+)(?:\/([<>]?=?)(.+))?$/', $sModuleId, $aModuleMatches)) {
- $sName = $aModuleMatches[1];
- $sOperator = $aModuleMatches[2] ?? null ?: '>=';
- $sExpectedVersion = $aModuleMatches[3] ?? null;
- }
-
- try {
- if (!isset(static::$sLastInstallDate)) {
- $aDatamodelResults = $oClient->Get('ModuleInstallation', ['name' => 'datamodel'], 'installed', 1);
- if ($aDatamodelResults['code'] != 0 || empty($aDatamodelResults['objects'])) {
- throw new Exception($aDatamodelResults['message'], $aDatamodelResults['code']);
- }
- $aDatamodel = current($aDatamodelResults['objects']);
- static::$sLastInstallDate = $aDatamodel['fields']['installed'];
- }
-
- $aResults = $oClient->Get('ModuleInstallation', ['name' => $sName, 'installed' => static::$sLastInstallDate], 'name,version', 1);
- if ($aResults['code'] != 0 || empty($aResults['objects'])) {
- throw new Exception($aResults['message'], $aResults['code']);
- }
- $aObject = current($aResults['objects']);
- $sCurrentVersion = $aObject['fields']['version'];
-
- if (isset($sExpectedVersion) && !version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) {
- throw new Exception(sprintf('Version mismatch (%s %s %s)', $sCurrentVersion, $sOperator, $sExpectedVersion));
- }
-
- Utils::Log(LOG_DEBUG, sprintf('iTop module %s version %s is installed.', $aObject['fields']['name'], $sCurrentVersion));
- } catch (Exception $e) {
- $sMessage = sprintf('%s iTop module %s is considered as not installed due to: %s', $bRequired ? 'Required' : 'Optional', $sName, $e->getMessage());
- if ($bRequired) {
- throw new Exception($sMessage, 0, $e);
- } else {
- Utils::Log(LOG_INFO, $sMessage);
- return false;
- }
- }
- return true;
- }
+ public static $iConsoleLogLevel = LOG_INFO;
+ public static $iSyslogLogLevel = LOG_NONE;
+ public static $iEventIssueLogLevel = LOG_NONE;
+ public static $sProjectName = "";
+ public static $sStep = "";
+ public static $oCollector = "";
+ protected static $oConfig = null;
+ protected static $aConfigFiles = [];
+ protected static $iMockLogLevel = LOG_ERR;
+
+ protected static $oMockedLogger;
+
+ /**
+ * @since 1.3.0 N°6012
+ */
+ protected static $oMockedDoPostRequestService;
+
+ /**
+ * @var string Keeps track of the latest date the datamodel has been installed/updated
+ * (in order to check which modules were installed with it)
+ */
+ protected static $sLastInstallDate;
+
+ public static function SetProjectName($sProjectName)
+ {
+ if ($sProjectName != null) {
+ self::$sProjectName = $sProjectName;
+ }
+ }
+
+ public static function SetCollector($oCollector, $sStep = "")
+ {
+ self::$oCollector = $oCollector;
+ self::$sStep = $sStep;
+ }
+
+ public static function ReadParameter($sParamName, $defaultValue)
+ {
+ global $argv;
+
+ $retValue = $defaultValue;
+ if (is_array($argv)) {
+ foreach ($argv as $iArg => $sArg) {
+ if (preg_match('/^--'.$sParamName.'=(.*)$/', $sArg, $aMatches)) {
+ $retValue = $aMatches[1];
+ }
+ }
+ }
+
+ return $retValue;
+ }
+
+ public static function ReadBooleanParameter($sParamName, $defaultValue)
+ {
+ global $argv;
+
+ $retValue = $defaultValue;
+ if (is_array($argv)) {
+ foreach ($argv as $iArg => $sArg) {
+ if (preg_match('/^--'.$sParamName.'$/', $sArg, $aMatches)) {
+ $retValue = true;
+ } elseif (preg_match('/^--'.$sParamName.'=(.*)$/', $sArg, $aMatches)) {
+ $retValue = ($aMatches[1] != 0);
+ }
+ }
+ }
+
+ return $retValue;
+ }
+
+ public static function CheckParameters($aOptionalParams)
+ {
+ global $argv;
+
+ $aUnknownParams = [];
+ if (is_array($argv)) {
+ foreach ($argv as $iArg => $sArg) {
+ if ($iArg == 0) {
+ continue;
+ } // Skip program name
+ if (preg_match('/^--([A-Za-z0-9_]+)$/', $sArg, $aMatches)) {
+ // Looks like a boolean parameter
+ if (!array_key_exists($aMatches[1], $aOptionalParams) || ($aOptionalParams[$aMatches[1]] != 'boolean')) {
+ $aUnknownParams[] = $sArg;
+ }
+ } elseif (preg_match('/^--([A-Za-z0-9_]+)=(.*)$/', $sArg, $aMatches)) {
+ // Looks like a regular parameter
+ if (!array_key_exists($aMatches[1], $aOptionalParams) || ($aOptionalParams[$aMatches[1]] == 'boolean')) {
+ $aUnknownParams[] = $sArg;
+ }
+ } else {
+ $aUnknownParams[] = $sArg;
+ }
+ }
+ }
+
+ return $aUnknownParams;
+ }
+
+ /**
+ * Init the console log level.
+ *
+ * Defaults to LOG_INFO if `console_log_level` is not configured
+ * Can be overridden by `console_log_level` commandline argument.
+ *
+ * @throws Exception
+ */
+ public static function InitConsoleLogLevel()
+ {
+ $iDefaultConsoleLogLevel = static::GetConfigurationValue('console_log_level', LOG_INFO);
+ static::$iConsoleLogLevel = static::ReadParameter('console_log_level', $iDefaultConsoleLogLevel);
+ }
+
+ /**
+ * Logs a message to the centralized log for the application, with the given priority
+ *
+ * @param int $iPriority Use the LOG_* constants for priority e.g. LOG_WARNING, LOG_INFO, LOG_ERR... (see:
+ * www.php.net/manual/en/function.syslog.php)
+ * @param string $sMessage The message to log
+ *
+ * @return void
+ * @throws \Exception
+ */
+ public static function Log($iPriority, $sMessage)
+ {
+ //testing only LOG_ERR
+ if (self::$oMockedLogger) {
+ if ($iPriority <= self::$iMockLogLevel) {
+ var_dump($sMessage);
+ self::$oMockedLogger->Log($iPriority, $sMessage);
+ }
+ }
+
+ switch ($iPriority) {
+ case LOG_EMERG:
+ $sPrio = 'Emergency';
+ break;
+
+ case LOG_ALERT:
+ $sPrio = 'Alert';
+ break;
+ case LOG_CRIT:
+ $sPrio = 'Critical Error';
+ break;
+
+ case LOG_ERR:
+ $sPrio = 'Error';
+ break;
+
+ case LOG_WARNING:
+ $sPrio = 'Warning';
+ break;
+
+ case LOG_NOTICE:
+ $sPrio = 'Notice';
+ break;
+
+ case LOG_INFO:
+ $sPrio = 'Info';
+ break;
+
+ case LOG_DEBUG:
+ $sPrio = 'Debug';
+ break;
+
+ default:
+ $sPrio = 'Critical Error';
+ }
+
+ if ($iPriority <= self::$iConsoleLogLevel) {
+ $log_date_format = self::GetConfigurationValue("console_log_dateformat", "[Y-m-d H:i:s]");
+ $txt = date($log_date_format)."\t[".$sPrio."]\t".$sMessage."\n";
+ echo $txt;
+ }
+
+ if ($iPriority <= self::$iSyslogLogLevel) {
+ openlog('iTop Data Collector', LOG_PID, LOG_USER);
+ syslog($iPriority, $sMessage);
+ closelog();
+ }
+
+ if ($iPriority <= self::$iEventIssueLogLevel) {
+ Utils::CreateEventIssue($sMessage);
+ }
+ }
+
+ /**
+ * @param bool $bResult
+ * @param string $sErrorMessage
+ */
+ private static function CreateEventIssue($sMessage)
+ {
+ $sProjectName = self::$sProjectName;
+ $sCollectorName = (self::$oCollector == null) ? "" : get_class(self::$oCollector);
+ $sStep = self::$sStep;
+
+ $aFields = [
+ "message" => "$sMessage",
+ "userinfo" => "Collector",
+ "issue" => "$sStep-$sCollectorName",
+ "impact" => "$sProjectName",
+ ];
+
+ $oClient = new RestClient();
+ $oClient->Create("EventIssue", $aFields, "create event issue from collector $sCollectorName execution.");
+ }
+
+ public static function MockLog($oMockedLogger, $iMockLogLevel = LOG_ERR)
+ {
+ self::$oMockedLogger = $oMockedLogger;
+ self::$iMockLogLevel = $iMockLogLevel;
+ }
+
+ /**
+ * @param DoPostRequestService|null $oMockedDoPostRequestService
+ * @since 1.3.0 N°6012
+ * @return void
+ */
+ public static function MockDoPostRequestService($oMockedDoPostRequestService)
+ {
+ self::$oMockedDoPostRequestService = $oMockedDoPostRequestService;
+ }
+
+ /**
+ * Load the configuration from the various XML configuration files
+ *
+ * @return Parameters
+ * @throws Exception
+ */
+ public static function LoadConfig()
+ {
+ $sCustomConfigFile = Utils::ReadParameter('config_file', null);
+
+ self::$aConfigFiles[] = CONF_DIR.'params.distrib.xml';
+ self::$oConfig = new Parameters(CONF_DIR.'params.distrib.xml');
+ if (file_exists(APPROOT.'collectors/params.distrib.xml')) {
+ self::MergeConfFile(APPROOT.'collectors/params.distrib.xml');
+ }
+ if (file_exists(APPROOT.'collectors/extensions/params.distrib.xml')) {
+ self::MergeConfFile(APPROOT.'collectors/extensions/params.distrib.xml');
+ }
+ if ($sCustomConfigFile !== null) {
+ // A custom config file was supplied on the command line
+ if (file_exists($sCustomConfigFile)) {
+ self::MergeConfFile($sCustomConfigFile);
+ } else {
+ throw new Exception("The specified configuration file '$sCustomConfigFile' does not exist.");
+ }
+ } elseif (file_exists(CONF_DIR.'params.local.xml')) {
+ self::MergeConfFile(CONF_DIR.'params.local.xml');
+ }
+
+ return self::$oConfig;
+ }
+
+ private static function MergeConfFile($sFilePath)
+ {
+ self::$aConfigFiles[] = $sFilePath;
+ $oLocalConfig = new Parameters($sFilePath);
+ self::$oConfig->Merge($oLocalConfig);
+ }
+
+ /**
+ * Get the value of a configuration parameter
+ *
+ * @param string $sCode
+ * @param mixed $defaultValue
+ *
+ * @return mixed
+ * @throws Exception
+ */
+ public static function GetConfigurationValue($sCode, $defaultValue = '')
+ {
+ if (self::$oConfig == null) {
+ self::LoadConfig();
+ }
+
+ $value = self::$oConfig->Get($sCode, $defaultValue);
+ $value = self::Substitute($value);
+
+ return $value;
+ }
+
+ /**
+ * @since 1.3.0 N°6012
+ */
+ public static function GetCredentials(): array
+ {
+ $sToken = Utils::GetConfigurationValue('itop_token', '');
+ if (strlen($sToken) > 0) {
+ return [
+ 'auth_token' => $sToken
+ ];
+ }
+
+ return [
+ 'auth_user' => Utils::GetConfigurationValue('itop_login', ''),
+ 'auth_pwd' => Utils::GetConfigurationValue('itop_password', ''),
+ ];
+ }
+
+ /**
+ * @since 1.3.0 N°6012
+ */
+ public static function GetLoginMode(): string
+ {
+ $sLoginform = Utils::GetConfigurationValue('itop_login_mode', '');
+ if (strlen($sLoginform) > 0) {
+ return $sLoginform;
+ }
+
+ $sToken = Utils::GetConfigurationValue('itop_token', '');
+ if (strlen($sToken) > 0) {
+ return 'token';
+ }
+
+ return 'form';
+ }
+
+ /**
+ * Dump information about the configuration (value of the parameters)
+ *
+ * @return string
+ * @throws Exception
+ */
+ public static function DumpConfig()
+ {
+ if (self::$oConfig == null) {
+ self::LoadConfig();
+ }
+
+ return self::$oConfig->Dump();
+ }
+
+ /**
+ * Get the ordered list of configuration files loaded
+ *
+ * @return string
+ * @throws Exception
+ */
+ public static function GetConfigFiles()
+ {
+ if (self::$oConfig == null) {
+ self::LoadConfig();
+ }
+
+ return self::$aConfigFiles;
+ }
+
+ protected static function Substitute($value)
+ {
+ if (is_array($value)) {
+ // Recursiverly process each entry
+ foreach ($value as $key => $val) {
+ $value[$key] = self::Substitute($val);
+ }
+ } elseif (is_string($value)) {
+ preg_match_all('/\$([A-Za-z0-9-_]+)\$/', $value, $aMatches);
+ $aReplacements = [];
+ if (count($aMatches) > 0) {
+ foreach ($aMatches[1] as $sSubCode) {
+ $aReplacements['$'.$sSubCode.'$'] = self::GetConfigurationValue($sSubCode, '#ERROR_UNDEFINED_PLACEHOLDER_'.$sSubCode.'#');
+ }
+ $value = str_replace(array_keys($aReplacements), $aReplacements, $value);
+ }
+ } else {
+ // Do nothing, return as-is
+ }
+
+ return $value;
+ }
+
+ /**
+ * Return the (valid) location where to store some temporary data
+ * Throws an exception if the directory specified in the 'data_path' configuration does not exist and cannot be created
+ *
+ * @param string $sFileName
+ *
+ * @return string
+ * @throws Exception
+ */
+ public static function GetDataFilePath($sFileName)
+ {
+ $sPath = static::GetConfigurationValue('data_path', '%APPROOT%/data/');
+ $sPath = str_replace('%APPROOT%', APPROOT, $sPath); // substitute the %APPROOT% placeholder with its actual value
+ $sPath = rtrim($sPath, '/').'/'; // Make that the path ends with exactly one /
+ if (!file_exists($sPath)) {
+ if (!mkdir($sPath, 0700, true)) {
+ throw new Exception("Failed to create data_path: '$sPath'. Either create the directory yourself or make sure that the script has enough rights to create it.");
+ }
+ }
+
+ return $sPath.basename($sFileName);
+ }
+
+ /**
+ * Helper to execute an HTTP POST request
+ * Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl
+ * originaly named after do_post_request
+ * Does not require cUrl but requires openssl for performing https POSTs.
+ *
+ * @param string $sUrl The URL to POST the data to
+ * @param hash $aData The data to POST as an array('param_name' => value)
+ * @param string $sOptionnalHeaders Additional HTTP headers as a string with newlines between headers
+ * @param hash $aResponseHeaders An array to be filled with reponse headers: WARNING: the actual content of the array depends on the library used: cURL or fopen, test with both !! See: http://fr.php.net/manual/en/function.curl-getinfo.php
+ * @param hash $aCurlOptions An (optional) array of options to pass to curl_init. The format is 'option_code' => 'value'. These values have precedence over the default ones
+ *
+ * @return string The result of the POST request
+ * @throws Exception
+ */
+ public static function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = [])
+ {
+ if (self::$oMockedDoPostRequestService) {
+ return self::$oMockedDoPostRequestService->DoPostRequest($sUrl, $aData, $sOptionnalHeaders, $aResponseHeaders, $aCurlOptions);
+ }
+
+ // $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request.
+
+ if (function_exists('curl_init')) {
+ // If cURL is available, let's use it, since it provides a greater control over the various HTTP/SSL options
+ // For instance fopen does not allow to work around the bug: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112
+ // by setting the SSLVERSION to 3 as done below.
+ $aHeaders = explode("\n", $sOptionnalHeaders);
+ // N°3267 - Webservices: Fix optional headers not being taken into account
+ // See https://www.php.net/curl_setopt CURLOPT_HTTPHEADER
+ $aHTTPHeaders = [];
+ foreach ($aHeaders as $sHeaderString) {
+ $aHTTPHeaders[] = trim($sHeaderString);
+ }
+ // Default options, can be overloaded/extended with the 4th parameter of this method, see above $aCurlOptions
+ $aOptions = [
+ CURLOPT_RETURNTRANSFER => true, // return the content of the request
+ CURLOPT_HEADER => false, // don't return the headers in the output
+ CURLOPT_FOLLOWLOCATION => true, // follow redirects
+ CURLOPT_ENCODING => "", // handle all encodings
+ CURLOPT_USERAGENT => "spider", // who am i
+ CURLOPT_AUTOREFERER => true, // set referer on redirect
+ CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect
+ CURLOPT_TIMEOUT => 120, // timeout on response
+ CURLOPT_MAXREDIRS => 10, // stop after 10 redirects
+ CURLOPT_SSL_VERIFYHOST => 0, // Disabled SSL Cert checks
+ CURLOPT_SSL_VERIFYPEER => 0, // Disabled SSL Cert checks
+ // SSLV3 (CURL_SSLVERSION_SSLv3 = 3) is now considered as obsolete/dangerous: http://disablessl3.com/#why
+ // but it used to be a MUST to prevent a strange SSL error: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112
+ // CURLOPT_SSLVERSION => 3,
+ CURLOPT_POST => count($aData),
+ CURLOPT_POSTFIELDS => http_build_query($aData),
+ CURLOPT_HTTPHEADER => $aHTTPHeaders,
+ ];
+
+ $aAllOptions = $aCurlOptions + $aOptions;
+ $ch = curl_init($sUrl);
+ curl_setopt_array($ch, $aAllOptions);
+ $response = curl_exec($ch);
+ $iErr = curl_errno($ch);
+ $sErrMsg = curl_error($ch);
+ $aHeaders = curl_getinfo($ch);
+ if ($iErr !== 0) {
+ throw new IOException("Problem opening URL: $sUrl"
+ .PHP_EOL." error msg: $sErrMsg"
+ .PHP_EOL." curl_init error code: $iErr (cf https://www.php.net/manual/en/function.curl-errno.php)");
+ }
+ if (is_array($aResponseHeaders)) {
+ $aHeaders = curl_getinfo($ch);
+ foreach ($aHeaders as $sCode => $sValue) {
+ $sName = str_replace(' ', '-', ucwords(str_replace('_', ' ', $sCode))); // Transform "content_type" into "Content-Type"
+ $aResponseHeaders[$sName] = $sValue;
+ }
+ }
+ curl_close($ch);
+ } else {
+ // cURL is not available let's try with streams and fopen...
+
+ $sData = http_build_query($aData);
+ $aParams = [
+ 'http' => [
+ 'method' => 'POST',
+ 'content' => $sData,
+ 'header' => "Content-type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($sData)."\r\n",
+ ],
+ ];
+ if ($sOptionnalHeaders !== null) {
+ $aParams['http']['header'] .= $sOptionnalHeaders;
+ }
+ $ctx = stream_context_create($aParams);
+
+ $fp = @fopen($sUrl, 'rb', false, $ctx);
+ if (!$fp) {
+ $error_arr = error_get_last();
+ if (is_array($error_arr)) {
+ throw new IOException("Wrong URL: $sUrl, Error: ".json_encode($error_arr));
+ } elseif ((strtolower(substr($sUrl, 0, 5)) == 'https') && !extension_loaded('openssl')) {
+ throw new IOException("Cannot connect to $sUrl: missing module 'openssl'");
+ } else {
+ throw new IOException("Wrong URL: $sUrl");
+ }
+ }
+ $response = @stream_get_contents($fp);
+ if ($response === false) {
+ throw new IOException("Problem reading data from $sUrl, " . error_get_last());
+ }
+ if (is_array($aResponseHeaders)) {
+ $aMeta = stream_get_meta_data($fp);
+ $aHeaders = $aMeta['wrapper_data'];
+ foreach ($aHeaders as $sHeaderString) {
+ if (preg_match('/^([^:]+): (.+)$/', $sHeaderString, $aMatches)) {
+ $aResponseHeaders[$aMatches[1]] = trim($aMatches[2]);
+ }
+ }
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Pretty print a JSON formatted string. Copied/pasted from http://stackoverflow.com/questions/6054033/pretty-printing-json-with-php
+ *
+ * @deprecated 1.3.0 use `json_encode($value, JSON_PRETTY_PRINT);` instead (PHP 5.4.0 required)
+ *
+ * @param string $json A JSON formatted object definition
+ *
+ * @return string The nicely formatted JSON definition
+ */
+ public static function JSONPrettyPrint($json)
+ {
+ Utils::Log(LOG_NOTICE, 'Use of deprecated method '.__METHOD__);
+
+ $result = '';
+ $level = 0;
+ $in_quotes = false;
+ $in_escape = false;
+ $ends_line_level = null;
+ $json_length = strlen($json);
+
+ for ($i = 0; $i < $json_length; $i++) {
+ $char = $json[$i];
+ $new_line_level = null;
+ $post = "";
+ if ($ends_line_level !== null) {
+ $new_line_level = $ends_line_level;
+ $ends_line_level = null;
+ }
+ if ($in_escape) {
+ $in_escape = false;
+ } elseif ($char === '"') {
+ $in_quotes = !$in_quotes;
+ } elseif (!$in_quotes) {
+ switch ($char) {
+ case '}':
+ case ']':
+ $level--;
+ $ends_line_level = null;
+ $new_line_level = $level;
+ break;
+
+ case '{':
+ case '[':
+ $level++;
+ // no break
+ case ',':
+ $ends_line_level = $level;
+ break;
+
+ case ':':
+ $post = " ";
+ break;
+
+ case " ":
+ case "\t":
+ case "\n":
+ case "\r":
+ $char = "";
+ $ends_line_level = $new_line_level;
+ $new_line_level = null;
+ break;
+ }
+ } elseif ($char === '\\') {
+ $in_escape = true;
+ }
+ if ($new_line_level !== null) {
+ $result .= "\n".str_repeat("\t", $new_line_level);
+ }
+ $result .= $char.$post;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Executes a command and returns an array with exit code, stdout and stderr content
+ *
+ * @param string $cmd - Command to execute
+ *
+ * @return false|string
+ * @throws \Exception
+ */
+ public static function Exec($sCmd)
+ {
+ $iBeginTime = time();
+ $sWorkDir = APPROOT;
+ $aDescriptorSpec = [
+ 0 => ["pipe", "r"], // stdin
+ 1 => ["pipe", "w"], // stdout
+ 2 => ["pipe", "w"], // stderr
+ ];
+ Utils::Log(LOG_INFO, "Command: $sCmd. Workdir: $sWorkDir");
+ $rProcess = proc_open($sCmd, $aDescriptorSpec, $aPipes, $sWorkDir, null);
+
+ $sStdOut = stream_get_contents($aPipes[1]);
+ fclose($aPipes[1]);
+
+ $sStdErr = stream_get_contents($aPipes[2]);
+ fclose($aPipes[2]);
+
+ $iCode = proc_close($rProcess);
+
+ $iElapsed = time() - $iBeginTime;
+ if (0 === $iCode) {
+ Utils::Log(LOG_INFO, "elapsed:{$iElapsed}s output: $sStdOut");
+
+ return $sStdOut;
+ } else {
+ throw new Exception("Command failed : $sCmd \n\t\t=== with status:$iCode \n\t\t=== stderr:$sStdErr \n\t\t=== stdout: $sStdOut");
+ }
+ }
+
+ /**
+ * @since 1.3.0
+ */
+ public static function GetCurlOptions(int $iCurrentTimeOut = -1): array
+ {
+ $aRawCurlOptions = Utils::GetConfigurationValue('curl_options', [CURLOPT_SSLVERSION => CURL_SSLVERSION_SSLv3]);
+ return self::ComputeCurlOptions($aRawCurlOptions, $iCurrentTimeOut);
+ }
+
+ /**
+ * @since 1.3.0
+ */
+ public static function ComputeCurlOptions(array $aRawCurlOptions, int $iCurrentTimeOut): array
+ {
+ $aCurlOptions = [];
+ foreach ($aRawCurlOptions as $key => $value) {
+ // Convert strings like 'CURLOPT_SSLVERSION' to the value of the corresponding define i.e CURLOPT_SSLVERSION = 32 !
+ $iKey = (!is_numeric($key)) ? constant((string)$key) : (int)$key;
+ $aCurlOptions[$iKey] = (!is_numeric($value) && defined($value)) ? constant($value) : $value;
+ }
+
+ if ($iCurrentTimeOut !== -1) {
+ $aCurlOptions[CURLOPT_CONNECTTIMEOUT] = $iCurrentTimeOut;
+ $aCurlOptions[CURLOPT_TIMEOUT] = $iCurrentTimeOut;
+ }
+
+ return $aCurlOptions;
+ }
+
+ /**
+ * Check if the given module is installed in iTop.
+ * Mind that this assumes the `ModuleInstallation` class is ordered by descending installation date
+ *
+ * @param string $sModuleId Name of the module to be found, optionally included version (e.g. "some-module" or "some-module/1.2.3")
+ * @param bool $bRequired Whether to throw exceptions when module not found
+ * @param RestClient|null $oClient
+ * @return bool True when the given module is installed, false otherwise
+ * @throws Exception When the module is required but could not be found
+ */
+ public static function CheckModuleInstallation(string $sModuleId, bool $bRequired = false, RestClient $oClient = null): bool
+ {
+ if (!isset($oClient)) {
+ $oClient = new RestClient();
+ }
+
+ $sName = '';
+ $sOperator = '';
+ if (preg_match('/^([^\/]+)(?:\/([<>]?=?)(.+))?$/', $sModuleId, $aModuleMatches)) {
+ $sName = $aModuleMatches[1];
+ $sOperator = $aModuleMatches[2] ?? null ?: '>=';
+ $sExpectedVersion = $aModuleMatches[3] ?? null;
+ }
+
+ try {
+ if (!isset(static::$sLastInstallDate)) {
+ $aDatamodelResults = $oClient->Get('ModuleInstallation', ['name' => 'datamodel'], 'installed', 1);
+ if ($aDatamodelResults['code'] != 0 || empty($aDatamodelResults['objects'])) {
+ throw new Exception($aDatamodelResults['message'], $aDatamodelResults['code']);
+ }
+ $aDatamodel = current($aDatamodelResults['objects']);
+ static::$sLastInstallDate = $aDatamodel['fields']['installed'];
+ }
+
+ $aResults = $oClient->Get('ModuleInstallation', ['name' => $sName, 'installed' => static::$sLastInstallDate], 'name,version', 1);
+ if ($aResults['code'] != 0 || empty($aResults['objects'])) {
+ throw new Exception($aResults['message'], $aResults['code']);
+ }
+ $aObject = current($aResults['objects']);
+ $sCurrentVersion = $aObject['fields']['version'];
+
+ if (isset($sExpectedVersion) && !version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) {
+ throw new Exception(sprintf('Version mismatch (%s %s %s)', $sCurrentVersion, $sOperator, $sExpectedVersion));
+ }
+
+ Utils::Log(LOG_DEBUG, sprintf('iTop module %s version %s is installed.', $aObject['fields']['name'], $sCurrentVersion));
+ } catch (Exception $e) {
+ $sMessage = sprintf('%s iTop module %s is considered as not installed due to: %s', $bRequired ? 'Required' : 'Optional', $sName, $e->getMessage());
+ if ($bRequired) {
+ throw new Exception($sMessage, 0, $e);
+ } else {
+ Utils::Log(LOG_INFO, $sMessage);
+ return false;
+ }
+ }
+ return true;
+ }
}
class UtilsLogger
{
- /**
- * UtilsLogger constructor.
- */
- public function __construct()
- {
- }
-
- public function Log($iPriority, $sMessage)
- {
- }
+ /**
+ * UtilsLogger constructor.
+ */
+ public function __construct()
+ {
+ }
+
+ public function Log($iPriority, $sMessage)
+ {
+ }
}
diff --git a/exec.php b/exec.php
index 645eeeb..95a84ba 100644
--- a/exec.php
+++ b/exec.php
@@ -35,38 +35,38 @@
require_once(APPROOT.'core/polyfill.inc.php');
$aOptionalParams = [
- 'configure_only' => 'boolean',
- 'collect_only' => 'boolean',
- 'synchro_only' => 'boolean',
- 'dump_config_only' => 'boolean',
- 'console_log_level' => 'integer',
- 'eventissue_log_level' => 'integer',
- 'max_chunk_size' => 'integer',
- 'help' => 'boolean',
- 'config_file' => 'string',
+ 'configure_only' => 'boolean',
+ 'collect_only' => 'boolean',
+ 'synchro_only' => 'boolean',
+ 'dump_config_only' => 'boolean',
+ 'console_log_level' => 'integer',
+ 'eventissue_log_level' => 'integer',
+ 'max_chunk_size' => 'integer',
+ 'help' => 'boolean',
+ 'config_file' => 'string',
];
$bHelp = (Utils::ReadBooleanParameter('help', false) == true);
$aUnknownParameters = Utils::CheckParameters($aOptionalParams);
if ($bHelp || count($aUnknownParameters) > 0) {
- if (!$bHelp) {
- Utils::Log(LOG_ERR, "Unknown parameter(s): ".implode(' ', $aUnknownParameters));
- }
-
- echo "Usage:\n";
- echo 'php '.basename($argv[0]);
- foreach ($aOptionalParams as $sParam => $sType) {
- switch ($sType) {
- case 'boolean':
- echo '[--'.$sParam.']';
- break;
-
- default:
- echo '[--'.$sParam.'=xxx]';
- break;
- }
- }
- echo "\n";
- exit(1);
+ if (!$bHelp) {
+ Utils::Log(LOG_ERR, "Unknown parameter(s): ".implode(' ', $aUnknownParameters));
+ }
+
+ echo "Usage:\n";
+ echo 'php '.basename($argv[0]);
+ foreach ($aOptionalParams as $sParam => $sType) {
+ switch ($sType) {
+ case 'boolean':
+ echo '[--'.$sParam.']';
+ break;
+
+ default:
+ echo '[--'.$sParam.'=xxx]';
+ break;
+ }
+ }
+ echo "\n";
+ exit(1);
}
$bResult = true;
@@ -77,59 +77,59 @@
$bDumpConfigOnly = (Utils::ReadBooleanParameter('dump_config_only', false) == true);
try {
- Utils::InitConsoleLogLevel();
- Utils::$iEventIssueLogLevel = Utils::ReadParameter('eventissue_log_level', Utils::GetConfigurationValue('eventissue_log_level', LOG_NONE));//On windows LOG_NOTICE=LOG_INFO=LOG_DEBUG=6
- $iMaxChunkSize = Utils::ReadParameter('max_chunk_size', Utils::GetConfigurationValue('max_chunk_size', 1000));
-
- if (file_exists(APPROOT.'collectors/main.php')) {
- require_once(APPROOT.'collectors/main.php');
- } else {
- Utils::Log(LOG_ERR, "The file '".APPROOT."collectors/main.php' is missing (or unreadable).");
- }
-
- if (!Orchestrator::CheckRequirements()) {
- exit(1);
- }
-
- $aConfig = Utils::GetConfigFiles();
- $sConfigDebug = "The following configuration files were loaded (in this order):\n\n";
- $idx = 1;
- foreach ($aConfig as $sFile) {
- $sConfigDebug .= "\t{$idx}. $sFile\n";
- $idx++;
- }
- $sConfigDebug .= "\nThe resulting configuration is:\n\n";
-
- $sConfigDebug .= Utils::DumpConfig();
-
- if ($bDumpConfigOnly) {
- echo $sConfigDebug;
- exit(0);
- } else {
- Utils::Log(LOG_DEBUG, $sConfigDebug);
- }
-
- $oOrchestrator = new Orchestrator();
- $aCollectors = $oOrchestrator->ListCollectors();
- Utils::Log(LOG_DEBUG, "Registered collectors:");
- foreach ($aCollectors as $oCollector) {
- Utils::Log(LOG_DEBUG, "Collector: ".$oCollector->GetName().", version: ".$oCollector->GetVersion());
- }
-
- if (!$bCollectOnly) {
- Utils::Log(LOG_DEBUG, 'iTop web services version: '.RestClient::GetNewestKnownVersion());
- $bResult = $oOrchestrator->InitSynchroDataSources($aCollectors);
- }
- if ($bResult && !$bSynchroOnly && !$bConfigureOnly) {
- $bResult = $oOrchestrator->Collect($aCollectors, $iMaxChunkSize, $bCollectOnly);
- }
-
- if ($bResult && !$bConfigureOnly && !$bCollectOnly) {
- $bResult = $oOrchestrator->Synchronize($aCollectors);
- }
+ Utils::InitConsoleLogLevel();
+ Utils::$iEventIssueLogLevel = Utils::ReadParameter('eventissue_log_level', Utils::GetConfigurationValue('eventissue_log_level', LOG_NONE));//On windows LOG_NOTICE=LOG_INFO=LOG_DEBUG=6
+ $iMaxChunkSize = Utils::ReadParameter('max_chunk_size', Utils::GetConfigurationValue('max_chunk_size', 1000));
+
+ if (file_exists(APPROOT.'collectors/main.php')) {
+ require_once(APPROOT.'collectors/main.php');
+ } else {
+ Utils::Log(LOG_ERR, "The file '".APPROOT."collectors/main.php' is missing (or unreadable).");
+ }
+
+ if (!Orchestrator::CheckRequirements()) {
+ exit(1);
+ }
+
+ $aConfig = Utils::GetConfigFiles();
+ $sConfigDebug = "The following configuration files were loaded (in this order):\n\n";
+ $idx = 1;
+ foreach ($aConfig as $sFile) {
+ $sConfigDebug .= "\t{$idx}. $sFile\n";
+ $idx++;
+ }
+ $sConfigDebug .= "\nThe resulting configuration is:\n\n";
+
+ $sConfigDebug .= Utils::DumpConfig();
+
+ if ($bDumpConfigOnly) {
+ echo $sConfigDebug;
+ exit(0);
+ } else {
+ Utils::Log(LOG_DEBUG, $sConfigDebug);
+ }
+
+ $oOrchestrator = new Orchestrator();
+ $aCollectors = $oOrchestrator->ListCollectors();
+ Utils::Log(LOG_DEBUG, "Registered collectors:");
+ foreach ($aCollectors as $oCollector) {
+ Utils::Log(LOG_DEBUG, "Collector: ".$oCollector->GetName().", version: ".$oCollector->GetVersion());
+ }
+
+ if (!$bCollectOnly) {
+ Utils::Log(LOG_DEBUG, 'iTop web services version: '.RestClient::GetNewestKnownVersion());
+ $bResult = $oOrchestrator->InitSynchroDataSources($aCollectors);
+ }
+ if ($bResult && !$bSynchroOnly && !$bConfigureOnly) {
+ $bResult = $oOrchestrator->Collect($aCollectors, $iMaxChunkSize, $bCollectOnly);
+ }
+
+ if ($bResult && !$bConfigureOnly && !$bCollectOnly) {
+ $bResult = $oOrchestrator->Synchronize($aCollectors);
+ }
} catch (Exception $e) {
- $bResult = false;
- Utils::Log(LOG_ERR, "Exception: ".$e->getMessage());
+ $bResult = false;
+ Utils::Log(LOG_ERR, "Exception: ".$e->getMessage());
}
exit($bResult ? 0 : 1); // exit code is zero means success
diff --git a/test/CallItopServiceTest.php b/test/CallItopServiceTest.php
index 6067831..84126b7 100644
--- a/test/CallItopServiceTest.php
+++ b/test/CallItopServiceTest.php
@@ -16,84 +16,84 @@
class CallItopServiceTest extends TestCase
{
- public function setUp(): void
- {
- parent::setUp();
- }
+ public function setUp(): void
+ {
+ parent::setUp();
+ }
- public function tearDown(): void
- {
- parent::tearDown();
- Utils::MockDoPostRequestService(null);
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ Utils::MockDoPostRequestService(null);
- $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
- $reflection->setAccessible(true);
- $reflection->setValue(null, null);
- }
+ $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
+ $reflection->setAccessible(true);
+ $reflection->setValue(null, null);
+ }
- public function GetCredentialsProvider()
- {
- return [
- 'login/password (nominal)' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2'
- ],
- 'aExpectedCredentials' => ['auth_user' => 'admin1', 'auth_pwd' => 'admin2']
- ],
- 'new token' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_token' => 'admin4',
- ],
- 'aExpectedCredentials' => ['auth_token' => 'admin4']
- ],
- 'new token over legacy one' => [
- 'aParameters' => [
- 'itop_login' => 'admin1',
- 'itop_password' => 'admin2',
- 'itop_rest_token' => 'admin3',
- 'itop_token' => 'admin4',
- ],
- 'aExpectedCredentials' => ['auth_token' => 'admin4']
- ],
- ];
- }
+ public function GetCredentialsProvider()
+ {
+ return [
+ 'login/password (nominal)' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2'
+ ],
+ 'aExpectedCredentials' => ['auth_user' => 'admin1', 'auth_pwd' => 'admin2']
+ ],
+ 'new token' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_token' => 'admin4',
+ ],
+ 'aExpectedCredentials' => ['auth_token' => 'admin4']
+ ],
+ 'new token over legacy one' => [
+ 'aParameters' => [
+ 'itop_login' => 'admin1',
+ 'itop_password' => 'admin2',
+ 'itop_rest_token' => 'admin3',
+ 'itop_token' => 'admin4',
+ ],
+ 'aExpectedCredentials' => ['auth_token' => 'admin4']
+ ],
+ ];
+ }
- /**
- * @dataProvider GetCredentialsProvider
- */
- public function testCallItopViaHttp($aParameters, $aExpectedCredentials)
- {
- $oParametersMock = $this->createMock(\Parameters::class);
- $oParametersMock->expects($this->atLeast(1))
- ->method('Get')
- ->will($this->returnCallback(
- function ($sKey, $aDefaultValue) use ($aParameters) {
- if (array_key_exists($sKey, $aParameters)) {
- return $aParameters[$sKey];
- }
- return $aDefaultValue;
- }
- ));
+ /**
+ * @dataProvider GetCredentialsProvider
+ */
+ public function testCallItopViaHttp($aParameters, $aExpectedCredentials)
+ {
+ $oParametersMock = $this->createMock(\Parameters::class);
+ $oParametersMock->expects($this->atLeast(1))
+ ->method('Get')
+ ->will($this->returnCallback(
+ function ($sKey, $aDefaultValue) use ($aParameters) {
+ if (array_key_exists($sKey, $aParameters)) {
+ return $aParameters[$sKey];
+ }
+ return $aDefaultValue;
+ }
+ ));
- $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
- $reflection->setAccessible(true);
- $reflection->setValue(null, $oParametersMock);
+ $reflection = new \ReflectionProperty(Utils::class, 'oConfig');
+ $reflection->setAccessible(true);
+ $reflection->setValue(null, $oParametersMock);
- $oMockedDoPostRequestService = $this->createMock(DoPostRequestService::class);
- Utils::MockDoPostRequestService($oMockedDoPostRequestService);
+ $oMockedDoPostRequestService = $this->createMock(DoPostRequestService::class);
+ Utils::MockDoPostRequestService($oMockedDoPostRequestService);
- $uri = 'http://itop.org';
- $aAdditionalData = ['gabu' => 'zomeu'];
- $oMockedDoPostRequestService->expects($this->once())
- ->method('DoPostRequest')
- ->with($uri, array_merge($aExpectedCredentials, $aAdditionalData))
- ;
+ $uri = 'http://itop.org';
+ $aAdditionalData = ['gabu' => 'zomeu'];
+ $oMockedDoPostRequestService->expects($this->once())
+ ->method('DoPostRequest')
+ ->with($uri, array_merge($aExpectedCredentials, $aAdditionalData))
+ ;
- $oCallItopService = new CallItopService();
- $oCallItopService->CallItopViaHttp($uri, $aAdditionalData);
- }
+ $oCallItopService = new CallItopService();
+ $oCallItopService->CallItopViaHttp($uri, $aAdditionalData);
+ }
}
diff --git a/test/CollectionPlanTest.php b/test/CollectionPlanTest.php
index c6725b3..0a394b4 100644
--- a/test/CollectionPlanTest.php
+++ b/test/CollectionPlanTest.php
@@ -20,135 +20,135 @@
class CollectionPlanTest extends TestCase
{
- private static $sCollectorPath = APPROOT."/collectors/";
- private $oMockedLogger;
-
- public function setUp(): void
- {
- parent::setUp();
-
- $this->CleanCollectorsFiles();
- }
-
- public function tearDown(): void
- {
- parent::tearDown();
- $this->CleanCollectorsFiles();
- }
-
- /**
- * @return void
- * @throws \IOException
- */
- public function testGetSortedLaunchSequence()
- {
- $this->CopyCollectorsFiles();
-
- require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
- $oTestCollectionPlan = new TestCollectionPlan();
- $oTestCollectionPlan->Init();
-
- $aCollectorsLaunchSequence = $oTestCollectionPlan->GetSortedLaunchSequence();
-
- foreach ($aCollectorsLaunchSequence as $iKey => $aCollector) {
- $this->assertFalse($aCollector['name'] == 'StandardCollectorWithNoRank');
- }
- $this->assertTrue($aCollectorsLaunchSequence[1]['name'] == 'ExtendedCollector');
- }
-
- /**
- * @return void
- * @throws \IOException
- */
- public function testGetCollectorDefinitionFile()
- {
- $this->CopyCollectorsFiles();
-
- require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
- $oTestCollectionPlan = new TestCollectionPlan();
- $oTestCollectionPlan->Init();
-
- $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('ExtendedCollector'));
- $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('StandardCollector'));
- $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('LegacyCollector'));
- $this->assertFalse($oTestCollectionPlan->GetCollectorDefinitionFile('OtherCollector'));
- }
-
- /**
- * @return void
- * @throws \IOException
- */
- public function testAddCollectorsToOrchestrator()
- {
- $aCollector = ['ExtendedCollector', 'StandardCollector', 'LegacyCollector', 'OtherCollector'];
-
- $this->CopyCollectorsFiles();
-
- require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
- $oTestCollectionPlan = new TestCollectionPlan();
- $oTestCollectionPlan->Init();
- $oTestCollectionPlan->AddCollectorsToOrchestrator();
-
- $oOrchestrator = new Orchestrator();
- $aOrchestratedCollectors = $oOrchestrator->ListCollectors();
-
- $this->assertArrayHasKey(0, $aOrchestratedCollectors);
- $this->assertTrue(($aOrchestratedCollectors[0] instanceof StandardCollector) || ($aOrchestratedCollectors[0] instanceof ExtendedCollector));
- $this->assertArrayHasKey(1, $aOrchestratedCollectors);
- $this->assertTrue(($aOrchestratedCollectors[1] instanceof StandardCollector) || ($aOrchestratedCollectors[1] instanceof ExtendedCollector));
- }
-
- private function CopyCollectorsFiles()
- {
- $aPatterns = [
- '' => '/test/collectionplan/collectors_files/*',
- 'extensions/' => '/test/collectionplan/collectors_files/extensions/*',
- 'extensions/src/' => '/test/collectionplan/collectors_files/extensions/src/*',
- 'extensions/json/' => '/test/collectionplan/collectors_files/extensions/json/*',
- 'src/' => '/test/collectionplan/collectors_files/src/*',
- 'json/' => '/test/collectionplan/collectors_files/json/*',
- ];
- foreach ($aPatterns as $sDir => $sPattern) {
- if (!is_dir(self::$sCollectorPath.$sDir)) {
- mkdir(self::$sCollectorPath.$sDir);
- }
- $this->CopyFile($sDir, APPROOT.$sPattern);
- }
- }
-
- private function CopyFile($sDir, $sPattern)
- {
- $aFiles = glob($sPattern);
- foreach ($aFiles as $sFile) {
- if (is_file($sFile)) {
- $bRes = copy($sFile, self::$sCollectorPath.'/'.$sDir.basename($sFile));
- if (!$bRes) {
- throw new Exception("Failed copying $sFile to ".self::$sCollectorPath.'/'.$sDir.basename($sFile));
- }
- }
- }
- }
-
- private function CleanCollectorsFiles()
- {
- $aPatterns = [
- 'extensions/src/' => 'extensions/src/*',
- 'extensions/json/' => 'extensions/json/*',
- 'extensions/' => 'extensions/*',
- 'src/' => 'src/*',
- 'json/' => 'json/*',
- '' => '*',
- ];
- foreach ($aPatterns as $sDir => $sPattern) {
- $aCollectorFiles = glob(self::$sCollectorPath.$sPattern);
- foreach ($aCollectorFiles as $sFile) {
- unlink($sFile);
- }
-
- if (is_dir(self::$sCollectorPath.$sDir)) {
- rmdir(self::$sCollectorPath.$sDir);
- }
- }
- }
+ private static $sCollectorPath = APPROOT."/collectors/";
+ private $oMockedLogger;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->CleanCollectorsFiles();
+ }
+
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ $this->CleanCollectorsFiles();
+ }
+
+ /**
+ * @return void
+ * @throws \IOException
+ */
+ public function testGetSortedLaunchSequence()
+ {
+ $this->CopyCollectorsFiles();
+
+ require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
+ $oTestCollectionPlan = new TestCollectionPlan();
+ $oTestCollectionPlan->Init();
+
+ $aCollectorsLaunchSequence = $oTestCollectionPlan->GetSortedLaunchSequence();
+
+ foreach ($aCollectorsLaunchSequence as $iKey => $aCollector) {
+ $this->assertFalse($aCollector['name'] == 'StandardCollectorWithNoRank');
+ }
+ $this->assertTrue($aCollectorsLaunchSequence[1]['name'] == 'ExtendedCollector');
+ }
+
+ /**
+ * @return void
+ * @throws \IOException
+ */
+ public function testGetCollectorDefinitionFile()
+ {
+ $this->CopyCollectorsFiles();
+
+ require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
+ $oTestCollectionPlan = new TestCollectionPlan();
+ $oTestCollectionPlan->Init();
+
+ $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('ExtendedCollector'));
+ $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('StandardCollector'));
+ $this->assertTrue($oTestCollectionPlan->GetCollectorDefinitionFile('LegacyCollector'));
+ $this->assertFalse($oTestCollectionPlan->GetCollectorDefinitionFile('OtherCollector'));
+ }
+
+ /**
+ * @return void
+ * @throws \IOException
+ */
+ public function testAddCollectorsToOrchestrator()
+ {
+ $aCollector = ['ExtendedCollector', 'StandardCollector', 'LegacyCollector', 'OtherCollector'];
+
+ $this->CopyCollectorsFiles();
+
+ require_once self::$sCollectorPath."src/TestCollectionPlan.class.inc.php";
+ $oTestCollectionPlan = new TestCollectionPlan();
+ $oTestCollectionPlan->Init();
+ $oTestCollectionPlan->AddCollectorsToOrchestrator();
+
+ $oOrchestrator = new Orchestrator();
+ $aOrchestratedCollectors = $oOrchestrator->ListCollectors();
+
+ $this->assertArrayHasKey(0, $aOrchestratedCollectors);
+ $this->assertTrue(($aOrchestratedCollectors[0] instanceof StandardCollector) || ($aOrchestratedCollectors[0] instanceof ExtendedCollector));
+ $this->assertArrayHasKey(1, $aOrchestratedCollectors);
+ $this->assertTrue(($aOrchestratedCollectors[1] instanceof StandardCollector) || ($aOrchestratedCollectors[1] instanceof ExtendedCollector));
+ }
+
+ private function CopyCollectorsFiles()
+ {
+ $aPatterns = [
+ '' => '/test/collectionplan/collectors_files/*',
+ 'extensions/' => '/test/collectionplan/collectors_files/extensions/*',
+ 'extensions/src/' => '/test/collectionplan/collectors_files/extensions/src/*',
+ 'extensions/json/' => '/test/collectionplan/collectors_files/extensions/json/*',
+ 'src/' => '/test/collectionplan/collectors_files/src/*',
+ 'json/' => '/test/collectionplan/collectors_files/json/*',
+ ];
+ foreach ($aPatterns as $sDir => $sPattern) {
+ if (!is_dir(self::$sCollectorPath.$sDir)) {
+ mkdir(self::$sCollectorPath.$sDir);
+ }
+ $this->CopyFile($sDir, APPROOT.$sPattern);
+ }
+ }
+
+ private function CopyFile($sDir, $sPattern)
+ {
+ $aFiles = glob($sPattern);
+ foreach ($aFiles as $sFile) {
+ if (is_file($sFile)) {
+ $bRes = copy($sFile, self::$sCollectorPath.'/'.$sDir.basename($sFile));
+ if (!$bRes) {
+ throw new Exception("Failed copying $sFile to ".self::$sCollectorPath.'/'.$sDir.basename($sFile));
+ }
+ }
+ }
+ }
+
+ private function CleanCollectorsFiles()
+ {
+ $aPatterns = [
+ 'extensions/src/' => 'extensions/src/*',
+ 'extensions/json/' => 'extensions/json/*',
+ 'extensions/' => 'extensions/*',
+ 'src/' => 'src/*',
+ 'json/' => 'json/*',
+ '' => '*',
+ ];
+ foreach ($aPatterns as $sDir => $sPattern) {
+ $aCollectorFiles = glob(self::$sCollectorPath.$sPattern);
+ foreach ($aCollectorFiles as $sFile) {
+ unlink($sFile);
+ }
+
+ if (is_dir(self::$sCollectorPath.$sDir)) {
+ rmdir(self::$sCollectorPath.$sDir);
+ }
+ }
+ }
}
diff --git a/test/CollectorSynchroTest.php b/test/CollectorSynchroTest.php
index 418a13a..ab8e636 100644
--- a/test/CollectorSynchroTest.php
+++ b/test/CollectorSynchroTest.php
@@ -17,15 +17,15 @@
class CollectorSynchroTest extends TestCase
{
- private $oMockedCallItopService;
+ private $oMockedCallItopService;
- public function SynchroOutputProvider()
- {
- $sRetcodeOutput = << [
- 'iConsoleLogLevel' => LOG_INFO,
- 'sExpectedOutputRequiredToItopSynchro' => 'retcode',
- 'sCallItopViaHttpOutput' => sprintf($sRetcodeOutput, 0)
- ],
- 'debug level' => [
- 'iConsoleLogLevel' => 7,
- 'sExpectedOutputRequiredToItopSynchro' => 'details',
- 'sCallItopViaHttpOutput' => sprintf($sDetailedNoError, 0)
- ],
- ];
- }
-
- /**
- * @dataProvider SynchroOutputProvider
- */
- public function testSynchroOutput($iConsoleLogLevel, $sExpectedOutputRequiredToItopSynchro, $sCallItopViaHttpOutput)
- {
- $oCollector = new \FakeCollector();
-
- Utils::$iConsoleLogLevel = $iConsoleLogLevel;
-
- $aAdditionalData = [
- 'separator' => ';',
- 'data_source_id' => 666,
- 'synchronize' => '0',
- 'no_stop_on_import_error' => 1,
- 'output' => $sExpectedOutputRequiredToItopSynchro,
- 'csvdata' => 'FAKECSVCONTENT',
- 'charset' => 'UTF-8',
- 'date_format' => 'd/m/Y'
- ];
- $this->oMockedCallItopService->expects($this->exactly(2))
- ->method('CallItopViaHttp')
- ->withConsecutive(
- ['/synchro/synchro_import.php?login_mode=form', $aAdditionalData],
- ['/synchro/synchro_exec.php?login_mode=form', ['data_sources' => 666], -1]
- )
- ->willReturn($sCallItopViaHttpOutput)
- ;
-
- $oCollector->Synchronize();
- }
-
- public function setUp(): void
- {
- global $argv;
- array_push($argv, "--config_file=".__DIR__."/utils/params.test.xml");
-
- parent::setUp();
- Utils::LoadConfig();
-
- $this->oMockedCallItopService = $this->createMock("CallItopService");
- Collector::SetCallItopService($this->oMockedCallItopService);
-
- $dataFilePath = Utils::GetDataFilePath('FakeCollector-*.csv');
- foreach (glob($dataFilePath) as $file) {
- unlink($file);
- }
-
- file_put_contents(APPROOT . 'data/FakeCollector-1.csv', 'FAKECSVCONTENT');
- }
-
- public function tearDown(): void
- {
- parent::tearDown();
- }
-
- public function ParseSynchroImportOutputProvider()
- {
- $sRetcodeOutput = << [
+ 'iConsoleLogLevel' => LOG_INFO,
+ 'sExpectedOutputRequiredToItopSynchro' => 'retcode',
+ 'sCallItopViaHttpOutput' => sprintf($sRetcodeOutput, 0)
+ ],
+ 'debug level' => [
+ 'iConsoleLogLevel' => 7,
+ 'sExpectedOutputRequiredToItopSynchro' => 'details',
+ 'sCallItopViaHttpOutput' => sprintf($sDetailedNoError, 0)
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider SynchroOutputProvider
+ */
+ public function testSynchroOutput($iConsoleLogLevel, $sExpectedOutputRequiredToItopSynchro, $sCallItopViaHttpOutput)
+ {
+ $oCollector = new \FakeCollector();
+
+ Utils::$iConsoleLogLevel = $iConsoleLogLevel;
+
+ $aAdditionalData = [
+ 'separator' => ';',
+ 'data_source_id' => 666,
+ 'synchronize' => '0',
+ 'no_stop_on_import_error' => 1,
+ 'output' => $sExpectedOutputRequiredToItopSynchro,
+ 'csvdata' => 'FAKECSVCONTENT',
+ 'charset' => 'UTF-8',
+ 'date_format' => 'd/m/Y'
+ ];
+ $this->oMockedCallItopService->expects($this->exactly(2))
+ ->method('CallItopViaHttp')
+ ->withConsecutive(
+ ['/synchro/synchro_import.php?login_mode=form', $aAdditionalData],
+ ['/synchro/synchro_exec.php?login_mode=form', ['data_sources' => 666], -1]
+ )
+ ->willReturn($sCallItopViaHttpOutput)
+ ;
+
+ $oCollector->Synchronize();
+ }
+
+ public function setUp(): void
+ {
+ global $argv;
+ array_push($argv, "--config_file=".__DIR__."/utils/params.test.xml");
+
+ parent::setUp();
+ Utils::LoadConfig();
+
+ $this->oMockedCallItopService = $this->createMock("CallItopService");
+ Collector::SetCallItopService($this->oMockedCallItopService);
+
+ $dataFilePath = Utils::GetDataFilePath('FakeCollector-*.csv');
+ foreach (glob($dataFilePath) as $file) {
+ unlink($file);
+ }
+
+ file_put_contents(APPROOT . 'data/FakeCollector-1.csv', 'FAKECSVCONTENT');
+ }
+
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ }
+
+ public function ParseSynchroImportOutputProvider()
+ {
+ $sRetcodeOutput = << [
- 'sOutput' => sprintf($sRetcodeOutput, 0),
- 'bDetailedOutput' => false,
- 'sExpectecCount' => 0
- ],
- 'retcode few errors' => [
- 'sOutput' => sprintf($sRetcodeOutput, 10),
- 'bDetailedOutput' => false,
- 'sExpectecCount' => 10
- ],
- 'detailed no error' => [
- 'sOutput' => sprintf($sDetailedNoError, 0),
- 'bDetailedOutput' => true,
- 'sExpectecCount' => 0
- ],
- 'detailed few errors' => [
- 'sOutput' => sprintf($sDetailedNoError, 10),
- 'bDetailedOutput' => true,
- 'sExpectecCount' => 10
- ],
- 'weird output' => [
- 'sOutput' => "weird output",
- 'bDetailedOutput' => true,
- 'sExpectecCount' => -1
- ],
- ];
- }
-
- /**
- * @dataProvider ParseSynchroImportOutputProvider
- */
- public function testParseSynchroImportOutput($sOutput, $bDetailedOutput, $sExpectecCount)
- {
- $this->assertEquals($sExpectecCount, Collector::ParseSynchroImportOutput($sOutput, $bDetailedOutput), $sOutput);
- }
-
- public function ParseSynchroExecOutput()
- {
- $sFailedOutput = << [
+ 'sOutput' => sprintf($sRetcodeOutput, 0),
+ 'bDetailedOutput' => false,
+ 'sExpectecCount' => 0
+ ],
+ 'retcode few errors' => [
+ 'sOutput' => sprintf($sRetcodeOutput, 10),
+ 'bDetailedOutput' => false,
+ 'sExpectecCount' => 10
+ ],
+ 'detailed no error' => [
+ 'sOutput' => sprintf($sDetailedNoError, 0),
+ 'bDetailedOutput' => true,
+ 'sExpectecCount' => 0
+ ],
+ 'detailed few errors' => [
+ 'sOutput' => sprintf($sDetailedNoError, 10),
+ 'bDetailedOutput' => true,
+ 'sExpectecCount' => 10
+ ],
+ 'weird output' => [
+ 'sOutput' => "weird output",
+ 'bDetailedOutput' => true,
+ 'sExpectecCount' => -1
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider ParseSynchroImportOutputProvider
+ */
+ public function testParseSynchroImportOutput($sOutput, $bDetailedOutput, $sExpectecCount)
+ {
+ $this->assertEquals($sExpectecCount, Collector::ParseSynchroImportOutput($sOutput, $bDetailedOutput), $sOutput);
+ }
+
+ public function ParseSynchroExecOutput()
+ {
+ $sFailedOutput = <<Working on Synchro LDAP Person (id=3)...
Replicas: 14
Replicas touched since last synchro: 0
Objects deleted: 0
Objects deletion errors: 1
Objects obsoleted: 0
Objects obsolescence errors: 2
Objects created: 0 (0 warnings)
Objects creation errors: 3
Objects updated: 0 (0 warnings)
Objects update errors: 4
Objects reconciled (updated): 0 (0 warnings)
Objects reconciled (unchanged): 0 (0 warnings)
Objects reconciliation errors: 5
Replica disappeared, no action taken: 0
TXT;
- $sFailedOutputWithNoErrorCount = <<Working on Synchro LDAP Person (id=3)...
ERROR: All records have been untouched for some time (all of the objects could be deleted). Please check that the process that writes into the synchronization table is still running. Operation cancelled.
Replicas: 14
Replicas touched since last synchro: 0
Objects deleted: 0
Objects deletion errors: 0
Objects obsoleted: 0
Objects obsolescence errors: 0
Objects created: 0 (0 warnings)
Objects creation errors: 0
Objects updated: 0 (0 warnings)
Objects update errors: 0
Objects reconciled (updated): 0 (0 warnings)
Objects reconciled (unchanged): 0 (0 warnings)
Objects reconciliation errors: 0
Replica disappeared, no action taken: 0
TXT;
- $sFailedNoMatch = <<Working on Synchro LDAP Person (id=3)...