diff --git a/src/includes/class/class.LScli.php b/src/includes/class/class.LScli.php index a16f2178..380b0054 100644 --- a/src/includes/class/class.LScli.php +++ b/src/includes/class/class.LScli.php @@ -738,6 +738,31 @@ class LScli extends LSlog_staticLoggerClass { return array(LScli :: quote_word("$rdn_attr=", $quote_char)); } + /** + * Autocomplete LSobject ioFormat option + * + * @param[in] $objType string LSobject type + * @param[in] $prefix string Option prefix (optional, default=empty string) + * @param[in] $case_sensitive boolean Set to false if options are case insensitive (optional, default=true) + * @param[in] $quote_char boolean Quote character (optional, if not set, $prefix will be unquoted and its + * quote char (if detected) will be used to quote options) + * + * @retval array List of available options + **/ + public static function autocomplete_LSobject_ioFormat($objType, $prefix='', $case_sensitive=true, $quote_char='') { + if (!LSsession ::loadLSobject($objType, false)) + return array(); + + // Make sure to unquote prefix + if (!$quote_char && $prefix) + $quote_char = self :: unquote_word($prefix); + + $obj = new $objType(); + $ioFormats = array_keys($obj -> listValidIOformats()); + + return self :: autocomplete_opts($ioFormats, $prefix, $case_sensitive, $quote_char); + } + /** * Unquote a word * diff --git a/src/includes/class/class.LSexport.php b/src/includes/class/class.LSexport.php index d284f48b..43909cc9 100644 --- a/src/includes/class/class.LSexport.php +++ b/src/includes/class/class.LSexport.php @@ -35,12 +35,13 @@ class LSexport extends LSlog_staticLoggerClass { * * @param[in] $LSobject LSldapObject An instance of the object type * @param[in] $ioFormat string The LSioFormat name + * @param[in] $stream resource|null The output stream (optional, default: STDOUT) * * @author Benjamin Renard * * @retval boolean True on success, False otherwise */ - public static function export($object, $ioFormat) { + public static function export($object, $ioFormat, $stream=null) { // Load LSobject if (is_string($object)) { if (!LSsession::loadLSobject($object, true)) { // Load with warning @@ -80,7 +81,7 @@ class LSexport extends LSlog_staticLoggerClass { self :: log_debug(count($objects)." object(s) found to export"); // Export objects using LSioFormat object - if (!$ioFormat -> exportObjects($objects)) { + if (!$ioFormat -> exportObjects($objects, $stream)) { LSerror :: addErrorCode('LSexport_04'); return false; } @@ -88,6 +89,110 @@ class LSexport extends LSlog_staticLoggerClass { return true; } + /** + * CLI export command + * + * @param[in] $command_args array Command arguments: + * - Positional arguments: + * - LSobject type + * - LSioFormat name + * - Optional arguments: + * - -o|--output: Output path ("-" == stdout, default: "-") + * + * @retval boolean True on succes, false otherwise + **/ + public static function cli_export($command_args) { + $objType = null; + $ioFormat = null; + $output = '-'; + for ($i=0; $i < count($command_args); $i++) { + switch ($command_args[$i]) { + case '-o': + case '--output': + $output = $command_args[++$i]; + break; + default: + if (is_null($objType)) { + $objType = $command_args[$i]; + } + elseif (is_null($ioFormat)) { + $ioFormat = $command_args[$i]; + } + else + LScli :: usage("Invalid $arg parameter."); + } + } + + if (is_null($objType) || is_null($ioFormat)) + LScli :: usage('You must provide LSobject type, ioFormat.'); + + // Check output + if ($output != '-' && file_exists($output)) + LScli :: usage("Output file '$output' already exists."); + + // Open output stream + $stream = fopen(($output=='-'?'php://stdout':$output), "w"); + if ($stream === false) + LSlog :: fatal("Fail to open output file '$output'."); + + // Run export + return self :: export($objType, $ioFormat, $stream); + } + + /** + * Args autocompleter for CLI export command + * + * @param[in] $command_args array List of already typed words of the command + * @param[in] $comp_word_num int The command word number to autocomplete + * @param[in] $comp_word string The command word to autocomplete + * @param[in] $opts array List of global available options + * + * @retval array List of available options for the word to autocomplete + **/ + public static function cli_export_args_autocompleter($command_args, $comp_word_num, $comp_word, $opts) { + $opts = array_merge($opts, array ('-o', '--output')); + + // Handle positional args + $objType = null; + $objType_arg_num = null; + $ioFormat = null; + $ioFormat_arg_num = null; + for ($i=0; $i < count($command_args); $i++) { + if (!in_array($command_args[$i], $opts)) { + // If object type not defined + if (is_null($objType)) { + // Defined it + $objType = $command_args[$i]; + LScli :: unquote_word($objType); + $objType_arg_num = $i; + + // Check object type exists + $objTypes = LScli :: autocomplete_LSobject_types($objType); + + // Load it if exist and not trying to complete it + if (in_array($objType, $objTypes) && $i != $comp_word_num) { + LSsession :: loadLSobject($objType, false); + } + } + elseif (is_null($ioFormat)) { + $ioFormat = $command_args[$i]; + LScli :: unquote_word($ioFormat); + $ioFormat_arg_num = $i; + } + } + } + + // If objType not already choiced (or currently autocomplete), add LSobject types to available options + if (!$objType || $objType_arg_num == $comp_word_num) + $opts = array_merge($opts, LScli :: autocomplete_LSobject_types($comp_word)); + + // If dn not alreay choiced (or currently autocomplete), try autocomplete it + elseif (!$ioFormat || $ioFormat_arg_num == $comp_word_num) + $opts = array_merge($opts, LScli :: autocomplete_LSobject_ioFormat($objType, $comp_word)); + + return LScli :: autocomplete_opts($opts, $comp_word); + } + } LSerror :: defineError('LSexport_01', ___("LSexport: input/output format %{format} invalid.") @@ -101,3 +206,25 @@ ___("LSexport: Fail to load objects's data to export from LDAP directory.") LSerror :: defineError('LSexport_04', ___("LSexport: Fail to export objects's data.") ); + +// Defined CLI commands functions only on CLI context +if (php_sapi_name() != 'cli') + return true; // Always return true to avoid some warning in log + +// LScli +LScli :: add_command( + 'export', + array('LSexport', 'cli_export'), + 'Export LSobject', + '[object type] [ioFormat name] -o /path/to/output.file', + array( + ' - Positional arguments :', + ' - LSobject type', + ' - LSioFormat name', + '', + ' - Optional arguments :', + ' - -o|--output: The output file path. Use "-" for STDOUT (optional, default: "-")', + ), + true, + array('LSexport', 'cli_export_args_autocompleter') +); diff --git a/src/includes/class/class.LSimport.php b/src/includes/class/class.LSimport.php index 9584b38b..3faecc46 100644 --- a/src/includes/class/class.LSimport.php +++ b/src/includes/class/class.LSimport.php @@ -102,8 +102,35 @@ class LSimport extends LSlog_staticLoggerClass { * * This method retreive, validate and import POST data. * - * If import could start, the return value is an array : + * @author Benjamin Renard * + * @retval array Array of the import result + * @see import() + */ + public static function importFromPostData() { + // Get data from $_POST + $data = self::getPostData(); + if (!is_array($data)) { + LSerror :: addErrorCode('LSimport_01'); + return array( + 'success' => false, + 'imported' => array(), + 'updated' => array(), + 'errors' => array(), + ); + } + self :: log_trace("importFromPostData(): POST data=".varDump($data)); + + return self :: import( + $data['LSobject'], $data['ioFormat'], $data['importfile'], + $data['updateIfExists'], $data['justTry'] + ); + } + + /** + * Import objects + * + * The return value is an array : * * array ( * 'success' => boolean, @@ -142,58 +169,61 @@ class LSimport extends LSlog_staticLoggerClass { * ) * ) * + * @param[in] $LSobject string The LSobject type + * @param[in] $ioFormat string The LSioFormat name + * @param[in] $input_file string|resource The input file path + * @param[in] $updateIfExists boolean If true and object to import already exists, update it. If false, + * an error will be triggered. (optional, default: false) + * @param[in] $justTry boolean If true, enable just-try mode: just check input data but do not really + * import objects in LDAP directory. (optional, default: false) + * * @author Benjamin Renard * * @retval array Array of the import result */ - public static function importFromPostData() { - // Get data from $_POST - $data = self::getPostData(); + public static function import($LSobject, $ioFormat, $input_file, $updateIfExists=false, $justTry=false) { $return = array( 'success' => false, + 'LSobject' => $LSobject, + 'ioFormat' => $ioFormat, + 'updateIfExists' => $updateIfExists, + 'justTry' => $justTry, 'imported' => array(), 'updated' => array(), 'errors' => array(), ); - if (!is_array($data)) { - LSerror :: addErrorCode('LSimport_01'); - return $return; - } - self :: log_trace("importFromPostData(): POST data=".varDump($data)); - $return = array_merge($return, $data); + // Load LSobject - if (!isset($data['LSobject']) || !LSsession::loadLSobject($data['LSobject'])) { + if (!isset($LSobject) || !LSsession::loadLSobject($LSobject)) { LSerror :: addErrorCode('LSimport_02'); return $return; } - $LSobject = $data['LSobject']; - // Validate ioFormat $object = new $LSobject(); - if(!$object -> isValidIOformat($data['ioFormat'])) { - LSerror :: addErrorCode('LSimport_03',$data['ioFormat']); + if(!$object -> isValidIOformat($ioFormat)) { + LSerror :: addErrorCode('LSimport_03',$ioFormat); return $return; } // Create LSioFormat object - $ioFormat = new LSioFormat($LSobject,$data['ioFormat']); + $ioFormat = new LSioFormat($LSobject,$ioFormat); if (!$ioFormat -> ready()) { LSerror :: addErrorCode('LSimport_04'); return $return; } // Load data in LSioFormat object - if (!$ioFormat -> loadFile($data['importfile'])) { + if (!$ioFormat -> loadFile($input_file)) { LSerror :: addErrorCode('LSimport_05'); return $return; } - self :: log_debug("importFromPostData(): file loaded"); + self :: log_debug("import(): file loaded"); // Retreive object from ioFormat $objectsData = $ioFormat -> getAll(); $objectsInError = array(); - self :: log_trace("importFromPostData(): objects data=".varDump($objectsData)); + self :: log_trace("import(): objects data=".varDump($objectsData)); // Browse inputed objects foreach($objectsData as $objData) { @@ -204,45 +234,45 @@ class LSimport extends LSlog_staticLoggerClass { $form = $object -> getForm('create', null, true); // Set form data from inputed data if (!$form -> setPostData($objData, true)) { - self :: log_debug('importFromPostData(): Failed to setPostData on: '.print_r($objData,True)); + self :: log_debug('import(): Failed to setPostData on: '.print_r($objData,True)); $globalErrors[] = _('Failed to set post data on creation form.'); } // Validate form else if (!$form -> validate(true)) { - self :: log_debug('importFromPostData(): Failed to validate form on: '.print_r($objData,True)); - self :: log_debug('importFromPostData(): Form errors: '.print_r($form->getErrors(),True)); + self :: log_debug('import(): Failed to validate form on: '.print_r($objData,True)); + self :: log_debug('import(): Form errors: '.print_r($form->getErrors(),True)); $globalErrors[] = _('Error validating creation form.'); } // Validate data (just check mode) else if (!$object -> updateData('create', True)) { - self :: log_debug('importFromPostData(): fail to validate object data: '.varDump($objData)); + self :: log_debug('import(): fail to validate object data: '.varDump($objData)); $globalErrors[] = _('Failed to validate object data.'); } else { - self :: log_debug('importFromPostData(): Data is correct, retreive object DN'); + self :: log_debug('import(): Data is correct, retreive object DN'); $dn = $object -> getDn(); if (!$dn) { - self :: log_debug('importFromPostData(): fail to generate for this object: '.varDump($objData)); + self :: log_debug('import(): fail to generate for this object: '.varDump($objData)); $globalErrors[] = _('Failed to generate DN for this object.'); } else { // Check if object already exists if (!LSldap :: exists($dn)) { // Creation mode - self :: log_debug('importFromPostData(): New object, perform creation'); - if ($data['justTry'] || $object -> updateData('create')) { + self :: log_debug('import(): New object, perform creation'); + if ($justTry || $object -> updateData('create')) { self :: log_info('Object '.$object -> getDn().' imported'); $return['imported'][$object -> getDn()] = $object -> getDisplayName(); continue; } else { - self :: log_error('Failed to updateData on : '.print_r($objData,True)); + self :: log_error('Failed to updateData on : '.print_r($objData, True)); $globalErrors[]=_('Error creating object on LDAP server.'); } } // This object already exist, check 'updateIfExists' mode - elseif (!$data['updateIfExists']) { - self :: log_debug('importFromPostData(): Object '.$dn.' already exist'); + elseif (!$updateIfExists) { + self :: log_debug('import(): Object '.$dn.' already exist'); $globalErrors[] = getFData(_('An object already exist on LDAP server with DN %{dn}.'),$dn); } else { @@ -253,31 +283,31 @@ class LSimport extends LSlog_staticLoggerClass { // Instanciate a new LSobject and load data from it's DN $object = new $LSobject(); if (!$object -> loadData($dn)) { - self :: log_debug('importFromPostData(): Failed to load data of '.$dn); + self :: log_debug('import(): Failed to load data of '.$dn); $globalErrors[] = getFData(_("Failed to load existing object %{dn} from LDAP server. Can't update object.")); } else { // Instanciate a modify form (in API mode) $form = $object -> getForm('modify', null, true); // Set form data from inputed data - if (!$form -> setPostData($objData,true)) { - self :: log_debug('importFromPostData(): Failed to setPostData on update form : '.print_r($objData,True)); + if (!$form -> setPostData($objData, true)) { + self :: log_debug('import(): Failed to setPostData on update form : '.print_r($objData, True)); $globalErrors[] = _('Failed to set post data on update form.'); } // Validate form else if (!$form -> validate(true)) { - self :: log_debug('importFromPostData(): Failed to validate update form on : '.print_r($objData,True)); - self :: log_debug('importFromPostData(): Form errors : '.print_r($form->getErrors(),True)); + self :: log_debug('import(): Failed to validate update form on : '.print_r($objData, True)); + self :: log_debug('import(): Form errors : '.print_r($form->getErrors(), True)); $globalErrors[] = _('Error validating update form.'); } // Update data on LDAP server - else if ($data['justTry'] || $object -> updateData('modify')) { + else if ($justTry || $object -> updateData('modify')) { self :: log_info('Object '.$object -> getDn().' updated'); $return['updated'][$object -> getDn()] = $object -> getDisplayName(); continue; } else { - self :: log_error('Object '.$object -> getDn().': Failed to updateData (modify) on : '.print_r($objData,True)); + self :: log_error('Object '.$object -> getDn().': Failed to updateData (modify) on : '.print_r($objData, True)); $globalErrors[] = _('Error updating object on LDAP server.'); } } @@ -297,6 +327,165 @@ class LSimport extends LSlog_staticLoggerClass { return $return; } + /** + * CLI import command + * + * @param[in] $command_args array Command arguments: + * - Positional arguments: + * - LSobject type + * - LSioFormat name + * - Optional arguments: + * - -i|--input: Input path ("-" == stdin) + * - -U|--update: Enable "update if exist" + * - -j|--just-try: Enable just-try mode + * + * @retval boolean True on succes, false otherwise + **/ + public static function cli_import($command_args) { + $objType = null; + $ioFormat = null; + $input = null; + $updateIfExists = false; + $justTry = false; + for ($i=0; $i < count($command_args); $i++) { + switch ($command_args[$i]) { + case '-i': + case '--input': + $input = $command_args[++$i]; + break; + case '-U': + case '--update': + $updateIfExists = true; + break; + case '-j': + case '--just-try': + $justTry = true; + break; + default: + if (is_null($objType)) { + $objType = $command_args[$i]; + } + elseif (is_null($ioFormat)) { + $ioFormat = $command_args[$i]; + } + else + LScli :: usage("Invalid '".$command_args[$i]."' parameter."); + } + } + + if (is_null($objType) || is_null($ioFormat)) + LScli :: usage('You must provide LSobject type, ioFormat.'); + + if (is_null($input)) + LScli :: usage('You must provide input path using -i/--input parameter.'); + + // Check output + if ($input != '-' && !is_file($input)) + LScli :: usage("Input file '$input' does not exists."); + + // Handle input from stdin + $input = ($input=='-'?'php://stdin':$input); + + // Run export + $result = self :: import($objType, $ioFormat, $input, $updateIfExists, $justTry); + + self :: log_info( + count($result['imported'])." object(s) imported, ".count($result['updated']). + " object(s) updated and ".count($result['errors'])." error(s) occurred." + ); + + if ($result['errors']) { + echo "Error(s):\n"; + foreach($result['errors'] as $idx => $obj) { + echo " - Object #$idx:\n"; + if ($obj['errors']['globals']) { + echo " - Global errors:\n"; + foreach ($obj['errors']['globals'] as $error) + echo " - $error\n"; + } + + echo " - Input data:\n"; + foreach ($obj['data'] as $key => $values) { + echo " - $key: ".(empty($values)?'No value':'"'.implode('", "', $values).'"')."\n"; + } + if ($obj['errors']['attrs']) { + echo " - Attribute errors:\n"; + foreach ($obj['errors']['attrs'] as $attr => $error) { + echo " - $attr: $error\n"; + } + } + } + } + + if ($result['imported']) { + echo count($result['imported'])." imported object(s):\n"; + foreach($result['imported'] as $dn => $name) + echo " - $name ($dn)\n"; + } + + if ($result['updated']) { + echo count($result['updated'])." updated object(s):\n"; + foreach($result['updated'] as $dn => $name) + echo " - $name ($dn)\n"; + } + + return $result['success']; + } + + /** + * Args autocompleter for CLI import command + * + * @param[in] $command_args array List of already typed words of the command + * @param[in] $comp_word_num int The command word number to autocomplete + * @param[in] $comp_word string The command word to autocomplete + * @param[in] $opts array List of global available options + * + * @retval array List of available options for the word to autocomplete + **/ + public static function cli_import_args_autocompleter($command_args, $comp_word_num, $comp_word, $opts) { + $opts = array_merge($opts, array ('-i', '--input', '-U', '--update', '-j', '--just-try')); + + // Handle positional args + $objType = null; + $objType_arg_num = null; + $ioFormat = null; + $ioFormat_arg_num = null; + for ($i=0; $i < count($command_args); $i++) { + if (!in_array($command_args[$i], $opts)) { + // If object type not defined + if (is_null($objType)) { + // Defined it + $objType = $command_args[$i]; + LScli :: unquote_word($objType); + $objType_arg_num = $i; + + // Check object type exists + $objTypes = LScli :: autocomplete_LSobject_types($objType); + + // Load it if exist and not trying to complete it + if (in_array($objType, $objTypes) && $i != $comp_word_num) { + LSsession :: loadLSobject($objType, false); + } + } + elseif (is_null($ioFormat)) { + $ioFormat = $command_args[$i]; + LScli :: unquote_word($ioFormat); + $ioFormat_arg_num = $i; + } + } + } + + // If objType not already choiced (or currently autocomplete), add LSobject types to available options + if (!$objType || $objType_arg_num == $comp_word_num) + $opts = array_merge($opts, LScli :: autocomplete_LSobject_types($comp_word)); + + // If dn not alreay choiced (or currently autocomplete), try autocomplete it + elseif (!$ioFormat || $ioFormat_arg_num == $comp_word_num) + $opts = array_merge($opts, LScli :: autocomplete_LSobject_ioFormat($objType, $comp_word)); + + return LScli :: autocomplete_opts($opts, $comp_word); + } + } @@ -338,3 +527,27 @@ ___("LSimport: Fail to initialize input/output driver.") LSerror :: defineError('LSimport_05', ___("LSimport: Fail to load objects's data from input file.") ); + +// Defined CLI commands functions only on CLI context +if (php_sapi_name() != 'cli') + return true; // Always return true to avoid some warning in log + +// LScli +LScli :: add_command( + 'import', + array('LSimport', 'cli_import'), + 'Import LSobject', + '[object type] [ioFormat name] -i /path/to/input.file', + array( + ' - Positional arguments :', + ' - LSobject type', + ' - LSioFormat name', + '', + ' - Optional arguments :', + ' - -i|--input The input file path. Use "-" for STDIN', + ' - -U|--update Enable "update if exist" mode', + ' - -j|--just-try Enable just-try mode', + ), + true, + array('LSimport', 'cli_import_args_autocompleter') +); diff --git a/src/includes/class/class.LSioFormat.php b/src/includes/class/class.LSioFormat.php index 2cbdb0b0..62ab1f1e 100644 --- a/src/includes/class/class.LSioFormat.php +++ b/src/includes/class/class.LSioFormat.php @@ -108,10 +108,11 @@ class LSioFormat extends LSlog_staticLoggerClass { * Export objects * * @param $objects array of LSldapObject The objects to export + * @param[in] $stream resource|null The output stream (optional, default: STDOUT) * * @return boolean True on succes, False otherwise */ - public function exportObjects(&$objects) { + public function exportObjects(&$objects, $stream=null) { self :: log_trace('exportObjects(): start'); $fields = $this -> getConfig('fields'); if (!$fields) { @@ -131,11 +132,11 @@ class LSioFormat extends LSlog_staticLoggerClass { // Add attributes to export and put their values to data to export foreach($fields as $key => $attr_name) { $object -> attrs[$attr_name] -> addToExport($export); - $objects_data[$object -> getDn()][$key] = $export -> elements[$attr_name] -> getApiValue(); + $objects_data[$object -> getDn()][$key] = $export -> elements[$attr_name] -> getApiValue(false); } } self :: log_trace('exportObjects(): objects data = '.varDump($objects_data)); - return $this -> driver -> exportObjectsData($objects_data); + return $this -> driver -> exportObjectsData($objects_data, $stream); } } diff --git a/src/includes/class/class.LSioFormatCSV.php b/src/includes/class/class.LSioFormatCSV.php index c140b64a..8c5c1cca 100644 --- a/src/includes/class/class.LSioFormatCSV.php +++ b/src/includes/class/class.LSioFormatCSV.php @@ -183,17 +183,23 @@ class LSioFormatCSV extends LSioFormatDriver { * * @param[in] $stream The stream where objects's data have to be exported * @param[in] $objects_data Array of objects data to export + * @param[in] $stream resource|null The output stream (optional, default: STDOUT) * * @return boolean True on succes, False otherwise */ - public function exportObjectsData($objects_data) { + public function exportObjectsData($objects_data, $stream=null) { if (!function_exists('fputcsv')) { LSerror :: addErrorCode('LSioFormatCSV_01'); return false; } + $stdout = false; + if (is_null($stream)) { + $stream = fopen('php://temp/maxmemory:'. (5*1024*1024), 'w+'); + $stdout = true; + } + $first = true; - $stream = fopen('php://temp/maxmemory:'. (5*1024*1024), 'w+'); foreach($objects_data as $dn => $object_data) { if ($first) { $this -> writeRow($stream, array_keys($object_data)); @@ -204,6 +210,8 @@ class LSioFormatCSV extends LSioFormatDriver { $row[] = (is_array($values)?implode($this -> multiple_value_delimiter, $values):$values); $this -> writeRow($stream, $row); } + if (!$stdout) + return true; header("Content-disposition: attachment; filename=export.csv"); header("Content-type: text/csv"); rewind($stream); diff --git a/src/includes/class/class.LSioFormatDriver.php b/src/includes/class/class.LSioFormatDriver.php index e8661222..e19de5b7 100644 --- a/src/includes/class/class.LSioFormatDriver.php +++ b/src/includes/class/class.LSioFormatDriver.php @@ -143,10 +143,11 @@ class LSioFormatDriver extends LSlog_staticLoggerClass { * Export objects data * * @param[in] $objects_data Array of objects data to export + * @param[in] $stream resource|null The output stream (optional, default: STDOUT) * * @return boolean True on succes, False otherwise */ - public function exportObjectsData($objects_data) { + public function exportObjectsData($objects_data, $stream=null) { // Must be implement in real drivers return False; }