Add import & export CLI commands

This commit is contained in:
Benjamin Renard 2021-02-05 18:12:44 +01:00
parent b8040e3d8b
commit f36c989136
6 changed files with 420 additions and 45 deletions

View file

@ -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
*

View file

@ -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 <brenard@easter-eggs.com>
*
* @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')
);

View file

@ -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 <brenard@easter-eggs.com>
*
* @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 <brenard@easter-eggs.com>
*
* @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')
);

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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;
}