From 2a922cb6d4ed612b44e5a4044f9c6136c629342e Mon Sep 17 00:00:00 2001
From: Thibault Dockx <thibault.dockx@fusiondirectory.org>
Date: Fri, 4 Apr 2025 17:04:54 +0100
Subject: [PATCH 1/3] :art: feat(extractor) - implement Extractor class for
 handling task extraction with support for multiple formats

---
 plugins/tasks/Extractor.php | 315 ++++++++++++++++++++++++++++++++++++
 1 file changed, 315 insertions(+)
 create mode 100644 plugins/tasks/Extractor.php

diff --git a/plugins/tasks/Extractor.php b/plugins/tasks/Extractor.php
new file mode 100644
index 0000000..1ad125e
--- /dev/null
+++ b/plugins/tasks/Extractor.php
@@ -0,0 +1,315 @@
+<?php
+
+class Extractor implements EndpointInterface
+{
+  private TaskGateway $gateway;
+
+  public function __construct(TaskGateway $gateway)
+  {
+    $this->gateway = $gateway;
+  }
+
+  /**
+   * @return array
+   * Part of the interface of orchestrator plugin to treat GET method
+   */
+  public function processEndPointGet(): array
+  {
+    // Retrieve tasks of type 'extract'
+    return $this->gateway->getObjectTypeTask('extract');
+  }
+
+  /**
+   * @param array|null $data
+   * @return array
+   * Note: Part of the interface of orchestrator plugin to treat POST method
+   */
+  public function processEndPointPost(array $data = NULL): array
+  {
+    return [];
+  }
+
+  /**
+   * @param array|null $data
+   * @return array
+   * Note: Part of the interface of orchestrator plugin to treat DELETE method
+   */
+  public function processEndPointDelete(array $data = NULL): array
+  {
+    return [];
+  }
+
+  /**
+   * @param array|null $data
+   * @return array
+   * @throws Exception
+   * Note: Part of the interface of orchestrator plugin to treat PATCH method
+   */
+  public function processEndPointPatch(array $data = NULL): array
+  {
+    $result = [];
+    $extractTasks = $this->gateway->getObjectTypeTask('extract');
+    
+    // Path is now expected in the JSON body ($data)
+    $path = $data['path'] ?? '/srv/orchestrator/';
+
+    foreach ($extractTasks as $task) {
+      try {
+        if (!$this->gateway->statusAndScheduleCheck($task)) {
+          // Skip this task if it does not meet the status and schedule criteria
+          continue;
+        }
+
+        // Get the main task configuration
+        $mainTaskConfig = $this->getExtractMainTaskConfig($task['fdtasksgranularmaster'][0]);
+        
+        // Get user DN from the task
+        $userDn = $task['fdtasksgranulardn'][0];
+        
+        // Get user attributes
+        $userAttributes = $this->getUserAttributes($userDn, $mainTaskConfig);
+        
+        // Format comes from the main task configuration
+        $format = isset($mainTaskConfig[0]['fdextractortaskformat']) ? 
+                 strtolower($mainTaskConfig[0]['fdextractortaskformat'][0]) : 'csv';
+        
+        // Create directory if it doesn't exist
+        $this->ensureDirectoryExists($path);
+        
+        // Get main task CN for filename
+        $mainTaskCn = $this->getMainTaskCn($task['fdtasksgranularmaster'][0]);
+        
+        // Determine filename with main task name and date with hours
+        $date = date('Y-m-d_H-i-s');
+        $filename = isset($data['filename']) ? 
+                   $path . $data['filename'] . '_' . $date . '.' . $format : 
+                   $path . $mainTaskCn . '_' . $date . '.' . $format;
+        
+        // Extract and write to file
+        $success = $this->extractToFile($userAttributes, $filename, $format);
+        
+        if ($success) {
+          $result[$task['dn']]['result'] = "User attributes successfully extracted to $filename";
+          $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], '2');
+        } else {
+          throw new Exception("Failed to write data to $filename");
+        }
+        
+      } catch (Exception $e) {
+        $result[$task['dn']]['result'] = "Error extracting user attributes: " . $e->getMessage();
+        $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], $e->getMessage());
+      }
+    }
+
+    return $result;
+  }
+
+  /**
+   * @param string $mainTaskDn
+   * @return array
+   * Note: Retrieve the configuration from the main extract task.
+   */
+  private function getExtractMainTaskConfig(string $mainTaskDn): array
+  {
+    return $this->gateway->getLdapTasks(
+      '(objectClass=fdExtractorTasks)',
+      ['fdExtractorTaskFormat', 'cn'],
+      '',
+      $mainTaskDn
+    );
+  }
+
+  /**
+   * @param string $userDn
+   * @param array $mainTaskConfig
+   * @return array
+   * Note: Get all user attributes from the user DN.
+   */
+  private function getUserAttributes(string $userDn, array $mainTaskConfig): array
+  {
+    // Get all user data from LDAP
+    $userData = $this->gateway->getLdapTasks(
+      '(objectClass=*)',
+      ['*'],
+      '',
+      $userDn
+    );
+    
+    // Process and return user data
+    $this->gateway->unsetCountKeys($userData);
+    return $userData;
+  }
+
+  /**
+   * @param string $path
+   * @return bool
+   * @throws Exception
+   * Note: Create directory if it doesn't exist.
+   */
+  private function ensureDirectoryExists(string $path): bool
+  {
+    if (!is_dir($path)) {
+      if (!mkdir($path, 0755, true)) {
+        throw new Exception("Failed to create directory: $path");
+      }
+    }
+    return true;
+  }
+
+  /**
+   * @param array $userAttributes
+   * @param string $filename
+   * @param string $format
+   * @return bool
+   * @throws Exception
+   * Note: Extract user attributes to a file.
+   */
+  private function extractToFile(array $userAttributes, string $filename, string $format): bool
+  {
+    switch (strtolower($format)) {
+      case 'csv':
+        return $this->exportToCsv($userAttributes, $filename);
+      case 'json':
+        return $this->exportToJson($userAttributes, $filename);
+      case 'xml':
+        return $this->exportToXml($userAttributes, $filename);
+      default:
+        return $this->exportToCsv($userAttributes, $filename);
+    }
+  }
+
+  /**
+   * @param array $userAttributes
+   * @param string $filename
+   * @return bool
+   * @throws Exception
+   * Note: Export user attributes to CSV.
+   */
+  private function exportToCsv(array $userAttributes, string $filename): bool
+  {
+    $fileExists = file_exists($filename);
+    $handle = fopen($filename, 'a');
+    
+    if ($handle === false) {
+      throw new Exception("Could not open file: $filename");
+    }
+    
+    try {
+      if (empty($userAttributes)) {
+        return true; // No attributes to write
+      }
+      
+      $user = $userAttributes[0];
+      $columns = [];
+      $data = [];
+      
+      // Prepare columns and data
+      foreach ($user as $attribute => $values) {
+        if (is_array($values)) {
+          foreach ($values as $key => $value) {
+            if (is_numeric($key)) {
+              $columns[] = $attribute;
+              $data[] = $value;
+              break; // Only take the first value for simplicity
+            }
+          }
+        }
+      }
+      
+      // Write header if file is new
+      if (!$fileExists) {
+        fputcsv($handle, $columns);
+      }
+      
+      // Write data
+      fputcsv($handle, $data);
+      
+      return true;
+    } finally {
+      fclose($handle);
+    }
+  }
+
+  /**
+   * @param array $userAttributes
+   * @param string $filename
+   * @return bool
+   * Note: Export user attributes to JSON.
+   */
+  private function exportToJson(array $userAttributes, string $filename): bool
+  {
+    $existingData = [];
+    
+    // Read existing data if file exists
+    if (file_exists($filename)) {
+      $existingJson = file_get_contents($filename);
+      if (!empty($existingJson)) {
+        $existingData = json_decode($existingJson, true) ?? [];
+      }
+    }
+    
+    // Add new data
+    $existingData[] = $userAttributes[0];
+    
+    // Write back to file
+    return file_put_contents($filename, json_encode($existingData, JSON_PRETTY_PRINT)) !== false;
+  }
+
+  /**
+   * @param array $userAttributes
+   * @param string $filename
+   * @return bool
+   * Note: Export user attributes to XML.
+   */
+  private function exportToXml(array $userAttributes, string $filename): bool
+  {
+    $dom = new DOMDocument('1.0', 'UTF-8');
+    $dom->formatOutput = true;
+    
+    // Create root element if file doesn't exist
+    if (file_exists($filename)) {
+      $dom->load($filename);
+      $root = $dom->documentElement;
+    } else {
+      $root = $dom->createElement('users');
+      $dom->appendChild($root);
+    }
+    
+    // Add new user element
+    $user = $dom->createElement('user');
+    
+    foreach ($userAttributes[0] as $attribute => $values) {
+      if (is_array($values)) {
+        foreach ($values as $key => $value) {
+          if (is_numeric($key)) {
+            $attr = $dom->createElement($attribute, htmlspecialchars($value));
+            $user->appendChild($attr);
+          }
+        }
+      }
+    }
+    
+    $root->appendChild($user);
+    
+    // Write to file
+    return $dom->save($filename) !== false;
+  }
+
+  /**
+   * Get the CN (Common Name) of the main task
+   * 
+   * @param string $mainTaskDn
+   * @return string
+   */
+  private function getMainTaskCn(string $mainTaskDn): string
+  {
+    $mainTask = $this->gateway->getLdapTasks(
+      '(objectClass=*)',
+      ['cn'],
+      '',
+      $mainTaskDn
+    );
+    
+    return $mainTask[0]['cn'][0] ?? 'extract';
+  }
+}
\ No newline at end of file
-- 
GitLab


From 1e3d445cdae931cccaaf3f27435b2c59bf41e0a4 Mon Sep 17 00:00:00 2001
From: Thibault Dockx <thibault.dockx@fusiondirectory.org>
Date: Fri, 4 Apr 2025 17:51:55 +0100
Subject: [PATCH 2/3] :art: feat(extractor) - enhance CSV, JSON, and XML export
 methods to handle duplicate UIDs and improve filename formatting

---
 plugins/tasks/Extractor.php | 152 ++++++++++++++++++++++++++++--------
 1 file changed, 118 insertions(+), 34 deletions(-)

diff --git a/plugins/tasks/Extractor.php b/plugins/tasks/Extractor.php
index 1ad125e..1faaa33 100644
--- a/plugins/tasks/Extractor.php
+++ b/plugins/tasks/Extractor.php
@@ -79,8 +79,8 @@ class Extractor implements EndpointInterface
         // Get main task CN for filename
         $mainTaskCn = $this->getMainTaskCn($task['fdtasksgranularmaster'][0]);
         
-        // Determine filename with main task name and date with hours
-        $date = date('Y-m-d_H-i-s');
+        // Determine filename with main task name and date with hour (no minutes or seconds)
+        $date = date('Y-m-d_H');  // Using only year-month-day_hour format
         $filename = isset($data['filename']) ? 
                    $path . $data['filename'] . '_' . $date . '.' . $format : 
                    $path . $mainTaskCn . '_' . $date . '.' . $format;
@@ -183,47 +183,99 @@ class Extractor implements EndpointInterface
    * @param string $filename
    * @return bool
    * @throws Exception
-   * Note: Export user attributes to CSV.
+   * Note: Export user attributes to CSV, preventing duplicate UIDs and handling new attributes.
    */
   private function exportToCsv(array $userAttributes, string $filename): bool
   {
-    $fileExists = file_exists($filename);
-    $handle = fopen($filename, 'a');
-    
-    if ($handle === false) {
-      throw new Exception("Could not open file: $filename");
+    if (empty($userAttributes)) {
+      return true; // No attributes to write
     }
     
-    try {
-      if (empty($userAttributes)) {
-        return true; // No attributes to write
+    $user = $userAttributes[0];
+    $userData = [];
+    $allColumns = [];
+    $existingData = [];
+    $uidKey = 'uid'; // The attribute to check for duplicates
+    $newUserUid = '';
+    
+    // Extract UID and prepare user data
+    foreach ($user as $attribute => $values) {
+      if (is_array($values)) {
+        foreach ($values as $key => $value) {
+          if (is_numeric($key)) {
+            $userData[$attribute] = $value;
+            $allColumns[$attribute] = true; // Use as associative array to avoid duplicates
+            
+            if (strtolower($attribute) === $uidKey) {
+              $newUserUid = $value;
+            }
+            
+            break; // Only take the first value for simplicity
+          }
+        }
       }
-      
-      $user = $userAttributes[0];
-      $columns = [];
-      $data = [];
-      
-      // Prepare columns and data
-      foreach ($user as $attribute => $values) {
-        if (is_array($values)) {
-          foreach ($values as $key => $value) {
-            if (is_numeric($key)) {
-              $columns[] = $attribute;
-              $data[] = $value;
-              break; // Only take the first value for simplicity
+    }
+    
+    // If no UID found, generate a random one
+    if (empty($newUserUid)) {
+      $newUserUid = 'user_' . uniqid();
+      $userData[$uidKey] = $newUserUid;
+      $allColumns[$uidKey] = true;
+    }
+    
+    // Read existing file if it exists
+    if (file_exists($filename)) {
+      $handle = fopen($filename, 'r');
+      if ($handle !== false) {
+        // Read headers
+        $headers = fgetcsv($handle);
+        if ($headers !== false) {
+          // Add existing headers to all columns
+          foreach ($headers as $header) {
+            $allColumns[$header] = true;
+          }
+          
+          // Read existing data
+          while (($row = fgetcsv($handle)) !== false) {
+            $rowData = [];
+            foreach ($headers as $index => $header) {
+              $rowData[$header] = $row[$index] ?? '';
+            }
+            // Only add to existing data if it's not the same UID as new user
+            if (isset($rowData[$uidKey]) && $rowData[$uidKey] !== $newUserUid) {
+              $existingData[] = $rowData;
             }
           }
         }
+        fclose($handle);
       }
+    }
+    
+    // Convert all columns associative array to indexed array
+    $finalColumns = array_keys($allColumns);
+    
+    // Add new user data to existing data
+    $existingData[] = $userData;
+    
+    // Write to file
+    $handle = fopen($filename, 'w'); // 'w' to overwrite with complete data
+    if ($handle === false) {
+      throw new Exception("Could not open file: $filename");
+    }
+    
+    try {
+      // Write headers
+      fputcsv($handle, $finalColumns);
       
-      // Write header if file is new
-      if (!$fileExists) {
-        fputcsv($handle, $columns);
+      // Write data rows
+      foreach ($existingData as $row) {
+        $outputRow = [];
+        foreach ($finalColumns as $column) {
+          $outputRow[] = $row[$column] ?? ''; // Use empty string if attribute not found
+        }
+        fputcsv($handle, $outputRow);
       }
       
-      // Write data
-      fputcsv($handle, $data);
-      
       return true;
     } finally {
       fclose($handle);
@@ -234,17 +286,31 @@ class Extractor implements EndpointInterface
    * @param array $userAttributes
    * @param string $filename
    * @return bool
-   * Note: Export user attributes to JSON.
+   * Note: Export user attributes to JSON with duplicate UID handling.
    */
   private function exportToJson(array $userAttributes, string $filename): bool
   {
     $existingData = [];
+    $uidKey = 'uid';
+    $newUserUid = '';
+    
+    // Get UID of new user
+    if (!empty($userAttributes[0][$uidKey][0])) {
+      $newUserUid = $userAttributes[0][$uidKey][0];
+    }
     
     // Read existing data if file exists
     if (file_exists($filename)) {
       $existingJson = file_get_contents($filename);
       if (!empty($existingJson)) {
-        $existingData = json_decode($existingJson, true) ?? [];
+        $jsonData = json_decode($existingJson, true) ?? [];
+        
+        // Filter out any entries with the same UID
+        foreach ($jsonData as $entry) {
+          if (!isset($entry[$uidKey][0]) || $entry[$uidKey][0] !== $newUserUid) {
+            $existingData[] = $entry;
+          }
+        }
       }
     }
     
@@ -259,17 +325,35 @@ class Extractor implements EndpointInterface
    * @param array $userAttributes
    * @param string $filename
    * @return bool
-   * Note: Export user attributes to XML.
+   * Note: Export user attributes to XML with duplicate UID handling.
    */
   private function exportToXml(array $userAttributes, string $filename): bool
   {
     $dom = new DOMDocument('1.0', 'UTF-8');
     $dom->formatOutput = true;
+    $uidKey = 'uid';
+    $newUserUid = '';
     
-    // Create root element if file doesn't exist
+    // Get UID of new user
+    if (!empty($userAttributes[0][$uidKey][0])) {
+      $newUserUid = $userAttributes[0][$uidKey][0];
+    }
+    
+    // Create or load XML document
     if (file_exists($filename)) {
       $dom->load($filename);
       $root = $dom->documentElement;
+      
+      // Remove any existing user with the same UID
+      if (!empty($newUserUid)) {
+        $xpath = new DOMXPath($dom);
+        $users = $xpath->query("/users/user[{$uidKey}='{$newUserUid}']");
+        if ($users->length > 0) {
+          foreach ($users as $user) {
+            $root->removeChild($user);
+          }
+        }
+      }
     } else {
       $root = $dom->createElement('users');
       $dom->appendChild($root);
-- 
GitLab


From a6a64fbaa76a5a5d2abb3a4d17949b51e7a91343 Mon Sep 17 00:00:00 2001
From: Thibault Dockx <thibault.dockx@fusiondirectory.org>
Date: Mon, 7 Apr 2025 14:05:34 +0100
Subject: [PATCH 3/3] :art: style(extractor) - improve code formatting by
 adding spaces in function signatures for better readability

---
 plugins/tasks/Extractor.php | 140 ++++++++++++++++++------------------
 1 file changed, 70 insertions(+), 70 deletions(-)

diff --git a/plugins/tasks/Extractor.php b/plugins/tasks/Extractor.php
index 1faaa33..d9b7d81 100644
--- a/plugins/tasks/Extractor.php
+++ b/plugins/tasks/Extractor.php
@@ -4,7 +4,7 @@ class Extractor implements EndpointInterface
 {
   private TaskGateway $gateway;
 
-  public function __construct(TaskGateway $gateway)
+  public function __construct (TaskGateway $gateway)
   {
     $this->gateway = $gateway;
   }
@@ -13,7 +13,7 @@ class Extractor implements EndpointInterface
    * @return array
    * Part of the interface of orchestrator plugin to treat GET method
    */
-  public function processEndPointGet(): array
+  public function processEndPointGet (): array
   {
     // Retrieve tasks of type 'extract'
     return $this->gateway->getObjectTypeTask('extract');
@@ -24,7 +24,7 @@ class Extractor implements EndpointInterface
    * @return array
    * Note: Part of the interface of orchestrator plugin to treat POST method
    */
-  public function processEndPointPost(array $data = NULL): array
+  public function processEndPointPost (array $data = NULL): array
   {
     return [];
   }
@@ -34,7 +34,7 @@ class Extractor implements EndpointInterface
    * @return array
    * Note: Part of the interface of orchestrator plugin to treat DELETE method
    */
-  public function processEndPointDelete(array $data = NULL): array
+  public function processEndPointDelete (array $data = NULL): array
   {
     return [];
   }
@@ -45,11 +45,11 @@ class Extractor implements EndpointInterface
    * @throws Exception
    * Note: Part of the interface of orchestrator plugin to treat PATCH method
    */
-  public function processEndPointPatch(array $data = NULL): array
+  public function processEndPointPatch (array $data = NULL): array
   {
     $result = [];
     $extractTasks = $this->gateway->getObjectTypeTask('extract');
-    
+
     // Path is now expected in the JSON body ($data)
     $path = $data['path'] ?? '/srv/orchestrator/';
 
@@ -62,39 +62,39 @@ class Extractor implements EndpointInterface
 
         // Get the main task configuration
         $mainTaskConfig = $this->getExtractMainTaskConfig($task['fdtasksgranularmaster'][0]);
-        
+
         // Get user DN from the task
         $userDn = $task['fdtasksgranulardn'][0];
-        
+
         // Get user attributes
         $userAttributes = $this->getUserAttributes($userDn, $mainTaskConfig);
-        
+
         // Format comes from the main task configuration
-        $format = isset($mainTaskConfig[0]['fdextractortaskformat']) ? 
+        $format = isset($mainTaskConfig[0]['fdextractortaskformat']) ?
                  strtolower($mainTaskConfig[0]['fdextractortaskformat'][0]) : 'csv';
-        
+
         // Create directory if it doesn't exist
         $this->ensureDirectoryExists($path);
-        
+
         // Get main task CN for filename
         $mainTaskCn = $this->getMainTaskCn($task['fdtasksgranularmaster'][0]);
-        
+
         // Determine filename with main task name and date with hour (no minutes or seconds)
         $date = date('Y-m-d_H');  // Using only year-month-day_hour format
-        $filename = isset($data['filename']) ? 
-                   $path . $data['filename'] . '_' . $date . '.' . $format : 
+        $filename = isset($data['filename']) ?
+                   $path . $data['filename'] . '_' . $date . '.' . $format :
                    $path . $mainTaskCn . '_' . $date . '.' . $format;
-        
+
         // Extract and write to file
         $success = $this->extractToFile($userAttributes, $filename, $format);
-        
+
         if ($success) {
           $result[$task['dn']]['result'] = "User attributes successfully extracted to $filename";
           $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], '2');
         } else {
           throw new Exception("Failed to write data to $filename");
         }
-        
+
       } catch (Exception $e) {
         $result[$task['dn']]['result'] = "Error extracting user attributes: " . $e->getMessage();
         $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], $e->getMessage());
@@ -109,7 +109,7 @@ class Extractor implements EndpointInterface
    * @return array
    * Note: Retrieve the configuration from the main extract task.
    */
-  private function getExtractMainTaskConfig(string $mainTaskDn): array
+  private function getExtractMainTaskConfig (string $mainTaskDn): array
   {
     return $this->gateway->getLdapTasks(
       '(objectClass=fdExtractorTasks)',
@@ -125,7 +125,7 @@ class Extractor implements EndpointInterface
    * @return array
    * Note: Get all user attributes from the user DN.
    */
-  private function getUserAttributes(string $userDn, array $mainTaskConfig): array
+  private function getUserAttributes (string $userDn, array $mainTaskConfig): array
   {
     // Get all user data from LDAP
     $userData = $this->gateway->getLdapTasks(
@@ -134,7 +134,7 @@ class Extractor implements EndpointInterface
       '',
       $userDn
     );
-    
+
     // Process and return user data
     $this->gateway->unsetCountKeys($userData);
     return $userData;
@@ -146,14 +146,14 @@ class Extractor implements EndpointInterface
    * @throws Exception
    * Note: Create directory if it doesn't exist.
    */
-  private function ensureDirectoryExists(string $path): bool
+  private function ensureDirectoryExists (string $path): bool
   {
     if (!is_dir($path)) {
-      if (!mkdir($path, 0755, true)) {
+      if (!mkdir($path, 0755, TRUE)) {
         throw new Exception("Failed to create directory: $path");
       }
     }
-    return true;
+    return TRUE;
   }
 
   /**
@@ -164,7 +164,7 @@ class Extractor implements EndpointInterface
    * @throws Exception
    * Note: Extract user attributes to a file.
    */
-  private function extractToFile(array $userAttributes, string $filename, string $format): bool
+  private function extractToFile (array $userAttributes, string $filename, string $format): bool
   {
     switch (strtolower($format)) {
       case 'csv':
@@ -185,58 +185,58 @@ class Extractor implements EndpointInterface
    * @throws Exception
    * Note: Export user attributes to CSV, preventing duplicate UIDs and handling new attributes.
    */
-  private function exportToCsv(array $userAttributes, string $filename): bool
+  private function exportToCsv (array $userAttributes, string $filename): bool
   {
     if (empty($userAttributes)) {
-      return true; // No attributes to write
+      return TRUE; // No attributes to write
     }
-    
+
     $user = $userAttributes[0];
     $userData = [];
     $allColumns = [];
     $existingData = [];
     $uidKey = 'uid'; // The attribute to check for duplicates
     $newUserUid = '';
-    
+
     // Extract UID and prepare user data
     foreach ($user as $attribute => $values) {
       if (is_array($values)) {
         foreach ($values as $key => $value) {
           if (is_numeric($key)) {
-            $userData[$attribute] = $value;
-            $allColumns[$attribute] = true; // Use as associative array to avoid duplicates
-            
+            $userData[$attribute]   = $value;
+            $allColumns[$attribute] = TRUE; // Use as associative array to avoid duplicates
+
             if (strtolower($attribute) === $uidKey) {
               $newUserUid = $value;
             }
-            
+
             break; // Only take the first value for simplicity
           }
         }
       }
     }
-    
+
     // If no UID found, generate a random one
     if (empty($newUserUid)) {
       $newUserUid = 'user_' . uniqid();
       $userData[$uidKey] = $newUserUid;
-      $allColumns[$uidKey] = true;
+      $allColumns[$uidKey] = TRUE;
     }
-    
+
     // Read existing file if it exists
     if (file_exists($filename)) {
       $handle = fopen($filename, 'r');
-      if ($handle !== false) {
+      if ($handle !== FALSE) {
         // Read headers
         $headers = fgetcsv($handle);
-        if ($headers !== false) {
+        if ($headers !== FALSE) {
           // Add existing headers to all columns
           foreach ($headers as $header) {
-            $allColumns[$header] = true;
+            $allColumns[$header] = TRUE;
           }
-          
+
           // Read existing data
-          while (($row = fgetcsv($handle)) !== false) {
+          while (($row = fgetcsv($handle)) !== FALSE) {
             $rowData = [];
             foreach ($headers as $index => $header) {
               $rowData[$header] = $row[$index] ?? '';
@@ -250,23 +250,23 @@ class Extractor implements EndpointInterface
         fclose($handle);
       }
     }
-    
+
     // Convert all columns associative array to indexed array
     $finalColumns = array_keys($allColumns);
-    
+
     // Add new user data to existing data
     $existingData[] = $userData;
-    
+
     // Write to file
     $handle = fopen($filename, 'w'); // 'w' to overwrite with complete data
-    if ($handle === false) {
+    if ($handle === FALSE) {
       throw new Exception("Could not open file: $filename");
     }
-    
+
     try {
       // Write headers
       fputcsv($handle, $finalColumns);
-      
+
       // Write data rows
       foreach ($existingData as $row) {
         $outputRow = [];
@@ -275,8 +275,8 @@ class Extractor implements EndpointInterface
         }
         fputcsv($handle, $outputRow);
       }
-      
-      return true;
+
+      return TRUE;
     } finally {
       fclose($handle);
     }
@@ -288,23 +288,23 @@ class Extractor implements EndpointInterface
    * @return bool
    * Note: Export user attributes to JSON with duplicate UID handling.
    */
-  private function exportToJson(array $userAttributes, string $filename): bool
+  private function exportToJson (array $userAttributes, string $filename): bool
   {
     $existingData = [];
     $uidKey = 'uid';
     $newUserUid = '';
-    
+
     // Get UID of new user
     if (!empty($userAttributes[0][$uidKey][0])) {
       $newUserUid = $userAttributes[0][$uidKey][0];
     }
-    
+
     // Read existing data if file exists
     if (file_exists($filename)) {
       $existingJson = file_get_contents($filename);
       if (!empty($existingJson)) {
-        $jsonData = json_decode($existingJson, true) ?? [];
-        
+        $jsonData = json_decode($existingJson, TRUE) ?? [];
+
         // Filter out any entries with the same UID
         foreach ($jsonData as $entry) {
           if (!isset($entry[$uidKey][0]) || $entry[$uidKey][0] !== $newUserUid) {
@@ -313,12 +313,12 @@ class Extractor implements EndpointInterface
         }
       }
     }
-    
+
     // Add new data
     $existingData[] = $userAttributes[0];
-    
+
     // Write back to file
-    return file_put_contents($filename, json_encode($existingData, JSON_PRETTY_PRINT)) !== false;
+    return file_put_contents($filename, json_encode($existingData, JSON_PRETTY_PRINT)) !== FALSE;
   }
 
   /**
@@ -327,23 +327,23 @@ class Extractor implements EndpointInterface
    * @return bool
    * Note: Export user attributes to XML with duplicate UID handling.
    */
-  private function exportToXml(array $userAttributes, string $filename): bool
+  private function exportToXml (array $userAttributes, string $filename): bool
   {
     $dom = new DOMDocument('1.0', 'UTF-8');
-    $dom->formatOutput = true;
+    $dom->formatOutput = TRUE;
     $uidKey = 'uid';
     $newUserUid = '';
-    
+
     // Get UID of new user
     if (!empty($userAttributes[0][$uidKey][0])) {
       $newUserUid = $userAttributes[0][$uidKey][0];
     }
-    
+
     // Create or load XML document
     if (file_exists($filename)) {
       $dom->load($filename);
       $root = $dom->documentElement;
-      
+
       // Remove any existing user with the same UID
       if (!empty($newUserUid)) {
         $xpath = new DOMXPath($dom);
@@ -358,10 +358,10 @@ class Extractor implements EndpointInterface
       $root = $dom->createElement('users');
       $dom->appendChild($root);
     }
-    
+
     // Add new user element
     $user = $dom->createElement('user');
-    
+
     foreach ($userAttributes[0] as $attribute => $values) {
       if (is_array($values)) {
         foreach ($values as $key => $value) {
@@ -372,20 +372,20 @@ class Extractor implements EndpointInterface
         }
       }
     }
-    
+
     $root->appendChild($user);
-    
+
     // Write to file
-    return $dom->save($filename) !== false;
+    return $dom->save($filename) !== FALSE;
   }
 
   /**
    * Get the CN (Common Name) of the main task
-   * 
+   *
    * @param string $mainTaskDn
    * @return string
    */
-  private function getMainTaskCn(string $mainTaskDn): string
+  private function getMainTaskCn (string $mainTaskDn): string
   {
     $mainTask = $this->gateway->getLdapTasks(
       '(objectClass=*)',
@@ -393,7 +393,7 @@ class Extractor implements EndpointInterface
       '',
       $mainTaskDn
     );
-    
+
     return $mainTask[0]['cn'][0] ?? 'extract';
   }
 }
\ No newline at end of file
-- 
GitLab