2010-10-13 19:24:32 +02:00
< ? php
/**
2016-05-01 19:47:59 +02:00
* EGroupWare - Plugin to import events from a CSV file
2011-04-11 17:21:10 +02:00
*
2010-10-13 19:24:32 +02:00
* @ license http :// opensource . org / licenses / gpl - license . php GPL - GNU General Public License
* @ package calendar
* @ subpackage importexport
* @ link http :// www . egroupware . org
* @ author Nathan Gray
* @ copyright Nathan Gray
* @ version $Id $
*/
2016-05-01 19:47:59 +02:00
use EGroupware\Api ;
2023-01-16 23:31:25 +01:00
use EGroupware\Api\Framework ;
2016-05-01 19:47:59 +02:00
use EGroupware\Api\Link ;
2010-10-13 19:24:32 +02:00
/**
* class import_csv for calendar
*/
2015-09-09 21:43:33 +02:00
class calendar_import_csv extends importexport_basic_import_csv {
2010-10-13 19:24:32 +02:00
/**
* actions wich could be done to data entries
*/
2015-09-09 21:43:33 +02:00
protected static $actions = array ( 'none' , 'update' , 'insert' );
2010-10-13 19:24:32 +02:00
/**
* conditions for actions
*
* @ var array
*/
2015-09-09 21:43:33 +02:00
protected static $conditions = array ( 'exists' );
2010-10-13 19:24:32 +02:00
/**
* For figuring out if an entry has changed
*/
protected $tracking ;
2011-12-15 17:24:47 +01:00
/**
* List of import warnings
*/
protected $warnings = array ();
2010-10-13 19:24:32 +02:00
/**
2019-04-24 22:14:35 +02:00
* Set up import
2021-03-28 20:48:55 +02:00
* @ param importexport_definition $definition
* @ param importexport_import_csv | null $import_csv
2012-05-15 15:23:11 +02:00
*/
2021-03-28 20:48:55 +02:00
protected function init ( importexport_definition $definition , importexport_import_csv $import_csv = NULL )
2015-09-09 21:43:33 +02:00
{
2010-10-13 19:24:32 +02:00
// fetch the addressbook bo
2023-01-16 23:31:25 +01:00
$this -> bo = new calendar_boupdate ();
2010-10-13 19:24:32 +02:00
// Get the tracker for changes
$this -> tracking = new calendar_tracking ();
2011-02-24 19:42:30 +01:00
// Used for participants
2023-01-16 23:31:25 +01:00
$this -> status_map = array_flip ( array_map ( 'lang' , $this -> bo -> verbose_status ));
2015-09-09 21:43:33 +02:00
$this -> role_map = array_flip ( $this -> bo -> roles );
2011-02-24 19:42:30 +01:00
2015-09-09 21:43:33 +02:00
$this -> lookups = array (
2023-01-16 23:31:25 +01:00
'priority' => array (
2013-01-31 15:13:45 +01:00
0 => lang ( 'None' ),
2011-04-07 01:12:50 +02:00
1 => lang ( 'Low' ),
2 => lang ( 'Normal' ),
3 => lang ( 'High' )
2023-01-16 23:31:25 +01:00
),
2012-10-12 19:11:29 +02:00
'recurrence' => $this -> bo -> recur_types
2011-04-07 01:12:50 +02:00
);
2015-09-09 21:43:33 +02:00
}
2011-04-07 01:12:50 +02:00
2015-09-09 21:43:33 +02:00
/**
* imports a single entry according to given definition object .
* Handles the conditions and the actions taken .
*
2017-01-31 17:16:03 +01:00
* @ param calendar_egw_record record The egw_record object being imported
2015-09-09 21:43:33 +02:00
* @ param importexport_iface_import_record import_csv Import object contains current state
*
* @ return boolean success
*/
2021-03-28 20:48:55 +02:00
public function import_record ( importexport_iface_egw_record & $record , & $import_csv )
2015-09-09 21:43:33 +02:00
{
2023-01-16 23:31:25 +01:00
if ( ! is_a ( $record , calendar_egw_record :: class ))
{
throw new TypeError ();
}
2015-09-09 21:43:33 +02:00
// set eventOwner
$options =& $this -> definition -> plugin_options ;
2023-01-16 23:31:25 +01:00
// Check options & set target calendar
// Make sure Owner from import dialog is not array
if ( is_array ( $options [ 'owner' ]))
{
$options [ 'owner' ] = array_pop ( $options [ 'owner' ]);
}
if ( $options [ 'owner' ] == '' )
{
$options [ 'owner_from_csv' ] = true ;
}
$options [ 'owner' ] = $options [ 'owner' ] ? ? $this -> user ;
2011-04-07 01:12:50 +02:00
2015-09-09 21:43:33 +02:00
// Set owner, unless it's supposed to come from CSV file
2023-01-16 23:31:25 +01:00
if ( $options [ 'owner_from_csv' ])
{
if ( ! is_numeric ( $record -> owner ))
{
2015-09-09 21:43:33 +02:00
$this -> errors [ $import_csv -> get_current_position ()] = lang (
'Invalid owner ID: %1. Might be a bad field translation. Used %2 instead.' ,
$record -> owner ,
$options [ 'owner' ]
);
$record -> owner = $options [ 'owner' ];
2010-10-13 19:24:32 +02:00
}
2015-09-09 21:43:33 +02:00
}
else
{
$record -> owner = $options [ 'owner' ];
}
2018-01-17 22:59:15 +01:00
2015-09-22 19:42:18 +02:00
// Handle errors in length or start/end date
2022-11-21 19:27:11 +01:00
if ( is_numeric ( $record -> start ) && is_numeric ( $record -> end ) && $record -> start > $record -> end )
2015-09-22 19:42:18 +02:00
{
$record -> end = $record -> start + $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'calendar' ][ 'defaultlength' ] * 60 ;
$this -> warnings [ $import_csv -> get_current_position ()] = lang ( 'error: starttime has to be before the endtime !!!' );
}
// Parse particpants
2015-09-09 21:43:33 +02:00
if ( $record -> participants && ! is_array ( $record -> participants )) {
2018-01-17 22:59:15 +01:00
$warning = '' ;
$record -> participants = $this -> parse_participants ( $record , $warning );
if ( $warning )
{
$this -> warnings [ $import_csv -> get_current_position ()] = $warning ;
2011-02-24 19:42:30 +01:00
}
2015-09-09 21:43:33 +02:00
}
2011-04-07 01:51:24 +02:00
2015-09-09 21:43:33 +02:00
if ( $record -> recurrence )
{
2017-04-20 18:01:32 +02:00
$start = new Api\DateTime ( $record -> start );
try
{
$rrule = calendar_rrule :: from_string ( $record -> recurrence , $start );
$record -> recur_type = $rrule -> type ;
$record -> recur_interval = $rrule -> interval ;
$record -> recur_enddate = $rrule -> enddate ;
$record -> recur_data = $rrule -> weekdays ;
$record -> recur_exception = $rrule -> exceptions ;
}
catch ( Exception $e )
{
// Try old way from export using just recur_type / interval
list ( $record -> recur_type , $record -> recur_interval ) = explode ( '/' , $record -> recurrence , 2 );
$record -> recur_interval = trim ( $record -> recur_interval );
$record -> recur_type = array_search ( strtolower ( trim ( $record -> recur_type )), array_map ( 'strtolower' , $this -> lookups [ 'recurrence' ]));
}
2015-09-09 21:43:33 +02:00
unset ( $record -> recurrence );
}
$record -> tzid = calendar_timezones :: id2tz ( $record -> tz_id );
2015-09-22 19:26:36 +02:00
if ( $options [ 'conditions' ] ) {
foreach ( $options [ 'conditions' ] as $condition ) {
2015-09-09 21:43:33 +02:00
$records = array ();
switch ( $condition [ 'type' ] ) {
// exists
case 'exists' :
// Check for that record
$result = $this -> exists ( $record , $condition , $records );
if ( is_array ( $records ) && count ( $records ) >= 1 ) {
// apply action to all records matching this exists condition
$action = $condition [ 'true' ];
foreach ( ( array ) $records as $event ) {
$record -> id = $event [ 'id' ];
if ( $this -> definition -> plugin_options [ 'update_cats' ] == 'add' ) {
if ( ! is_array ( $record -> category ) ) $record -> category = explode ( ',' , $record -> category );
$record -> category = implode ( ',' , array_unique ( array_merge ( $record -> category , $event [ 'category' ] ) ) );
2010-10-13 19:24:32 +02:00
}
2015-09-09 21:43:33 +02:00
$success = $this -> action ( $action [ 'action' ], $record , $import_csv -> get_current_position () );
2010-10-13 19:24:32 +02:00
}
2015-09-09 21:43:33 +02:00
} else {
$action = $condition [ 'false' ];
$success = ( $this -> action ( $action [ 'action' ], $record , $import_csv -> get_current_position () ));
}
break ;
// not supported action
default :
die ( 'condition / action not supported!!!' );
break ;
2010-10-13 19:24:32 +02:00
}
2015-09-09 21:43:33 +02:00
if ( $action [ 'last' ]) break ;
2010-10-13 19:24:32 +02:00
}
2015-09-09 21:43:33 +02:00
} else {
// unconditional insert
$success = $this -> action ( 'insert' , $record , $import_csv -> get_current_position () );
}
2016-05-01 19:47:59 +02:00
2015-09-09 21:43:33 +02:00
return $success ;
}
2018-01-17 22:59:15 +01:00
/**
* Parse participants field into calendar resources
*
* @ param string $participants
*
* @ return array
*/
protected function parse_participants ( $record , & $warnings )
{
// Importing participants in human friendly format:
// Name (quantity)? (status) Role[, Name (quantity)? (status) Role]+
$statuses = implode ( '|' , array_keys ( $this -> status_map ));
//echo ('/((?<name>.+?)(?: \((?<quantity>[\d]+)\))? \((?<status>'.$statuses.')\)(?: (?<role>[^ ,]+))?)(?:, )?/');
preg_match_all ( '/((?<name>.+?)(?: \((?<quantity>[\d]+)\))? \((?<status>' . $statuses . ')\)(?: (?<role>[^ ,]+))?)(?:, )?/i' , $record -> participants , $participants );
$p_participants = array ();
$missing = array ();
list ( $lines , $p , $names , $quantity , $status , $role ) = $participants ;
foreach ( $names as $key => $name ) {
//echo (__METHOD__ ." Name: $name Quantity: {$quantity[$key]} Status: {$status[$key]} Role: {$role[$key]}");
// Search for direct account name, then user in accounts first
$search = " \" $name\ " " ;
$id = importexport_helper_functions :: account_name2id ( $name );
// If not found, or not an exact match to a user (account_name2id is pretty generous)
if ( ! $id || $names [ $key ] !== $this -> bo -> participant_name ( $id )) {
$contacts = ExecMethod2 ( 'addressbook.addressbook_bo.search' , $search , array ( 'contact_id' , 'account_id' ), 'org_name,n_family,n_given,cat_id,contact_email' , '' , '%' , false , 'OR' , array ( 0 , 1 ));
if ( $contacts ) $id = $contacts [ 0 ][ 'account_id' ] ? $contacts [ 0 ][ 'account_id' ] : 'c' . $contacts [ 0 ][ 'contact_id' ];
}
if ( ! $id )
{
// Use calendar's registered resources to find participant
foreach ( $this -> bo -> resources as $resource )
{
// Can't search for email
if ( $resource [ 'app' ] == 'email' ) continue ;
// Special resource search, since it does special stuff in link_query
if ( $resource [ 'app' ] == 'resources' )
{
if ( ! $this -> resource_so )
{
$this -> resource_so = new resources_so ();
}
$result = $this -> resource_so -> search ( $search , 'res_id' );
2019-04-24 22:14:35 +02:00
if ( $result && count ( $result ) >= 1 ) {
2018-01-17 22:59:15 +01:00
$id = $resource [ 'type' ] . $result [ 0 ][ 'res_id' ];
break ;
}
}
else
{
// Search app via link query
$link_options = array ();
$result = Link :: query ( $resource [ 'app' ], $search , $link_options );
if ( $result )
{
$id = $resource [ 'type' ] . key ( $result );
break ;
}
}
}
}
if ( $id ) {
$p_participants [ $id ] = calendar_so :: combine_status (
$this -> status_map [ lang ( $status [ $key ])] ? $this -> status_map [ lang ( $status [ $key ])] : $status [ $key ][ 0 ],
$quantity [ $key ] ? $quantity [ $key ] : 1 ,
$this -> role_map [ lang ( $role [ $key ])] ? $this -> role_map [ lang ( $role [ $key ])] : $role [ $key ]
);
}
else
{
$missing [] = $name ;
}
if ( count ( $missing ) > 0 )
{
$warnings = $record -> title . ' ' . lang ( 'participants' ) . ': ' .
lang ( 'Contact not found!' ) . '<br />' . implode ( " , " , $missing );
}
}
return $p_participants ;
}
2015-09-09 21:43:33 +02:00
/**
* Search for matching records , based on the the given condition
*
* @ param record
* @ param condition array = array ( 'string' => field name )
* @ param matches - On return , will be filled with matching records
*
* @ return boolean
*/
protected function exists ( importexport_iface_egw_record & $record , Array & $condition , & $records = array ())
{
if ( $record -> __get ( $condition [ 'string' ]) && $condition [ 'string' ] == 'id' ) {
2015-09-22 19:26:36 +02:00
$event = $this -> bo -> read ( $record -> __get ( $condition [ 'string' ]));
2015-09-09 21:43:33 +02:00
$records = array ( $event );
}
if ( is_array ( $records ) && count ( $records ) >= 1 ) {
return true ;
2010-10-13 19:24:32 +02:00
}
2015-09-09 21:43:33 +02:00
return false ;
2010-10-13 19:24:32 +02:00
}
/**
* perform the required action
*
* @ param int $_action one of $this -> actions
* @ param array $_data record data for the action
* @ return bool success or not
*/
2015-09-09 21:43:33 +02:00
protected function action ( $_action , importexport_iface_egw_record & $record , $record_num = 0 )
{
$_data = $record -> get_record_array ();
2010-10-13 19:24:32 +02:00
switch ( $_action ) {
case 'none' :
return true ;
case 'update' :
// Only update if there are changes
$old = $this -> bo -> read ( $_data [ 'id' ]);
// Don't change a user account into a record
if ( ! $this -> definition -> plugin_options [ 'change_owner' ]) {
// Don't change owner of an existing record
unset ( $_data [ 'owner' ]);
}
// Merge to deal with fields not in import record
2016-07-25 20:23:31 +02:00
if ( $old )
{
$_data = array_merge ( $old , $_data );
$changed = $this -> tracking -> changed_fields ( $_data , $old );
if ( count ( $changed ) == 0 ) {
return true ;
}
2010-10-13 19:24:32 +02:00
}
// Fall through
case 'insert' :
if ( $_action == 'insert' ) {
// Backend doesn't like inserting with ID specified, can overwrite existing
unset ( $_data [ 'id' ]);
}
// Make sure participants are set
if ( ! $_data [ 'participants' ]) {
$user = $_data [ 'owner' ] ? $_data [ 'owner' ] : $this -> user ;
$_data [ 'participants' ] = array (
$user => 'U'
);
}
if ( $this -> dry_run ) {
//print_r($_data);
2016-07-11 21:08:41 +02:00
// User is interested in conflict checks, do so for dry run
// Otherwise, conflicts are just ignored and imported anyway
2016-07-20 19:29:32 +02:00
if ( $this -> definition -> plugin_options [ 'skip_conflicts' ] && ! $_data [ 'non_blocking' ])
2016-07-11 21:08:41 +02:00
{
$conflicts = $this -> bo -> conflicts ( $_data );
if ( $conflicts )
{
$this -> conflict_warning ( $record_num , $conflicts );
$this -> results [ 'skipped' ] ++ ;
return false ;
}
}
2010-10-13 19:24:32 +02:00
$this -> results [ $_action ] ++ ;
return true ;
} else {
2016-07-11 21:08:41 +02:00
$messages = null ;
2018-01-17 22:59:15 +01:00
$result = $this -> bo -> update ( $_data ,
2016-07-11 21:08:41 +02:00
! $this -> definition -> plugin_options [ 'skip_conflicts' ],
true , $this -> is_admin , true , $messages ,
$this -> definition -> plugin_options [ 'no_notification' ]
);
if ( ! $result )
{
2016-07-20 19:29:32 +02:00
$this -> errors [ $record_num ] = lang ( 'Unable to save' ) . " \n " .
implode ( " \n " , $messages );
2016-07-11 21:08:41 +02:00
}
else if ( is_array ( $result ))
{
$this -> conflict_warning ( $record_num , $result );
$this -> results [ 'skipped' ] ++ ;
return false ;
}
else
{
2010-10-13 19:24:32 +02:00
$this -> results [ $_action ] ++ ;
2015-09-09 21:43:33 +02:00
// This does nothing (yet?) but update the identifier
$record -> save ( $result );
2010-10-13 19:24:32 +02:00
}
return $result ;
}
default :
2016-05-01 19:47:59 +02:00
throw new Api\Exception ( 'Unsupported action' );
2012-05-15 15:23:11 +02:00
2010-10-13 19:24:32 +02:00
}
}
2018-01-17 22:59:15 +01:00
2016-07-11 21:08:41 +02:00
/**
* Add a warning message about conflicting events
2018-01-17 22:59:15 +01:00
*
2016-07-11 21:08:41 +02:00
* @ param int $record_num Current record index
* @ param Array $conflicts List of found conflicting events
*/
protected function conflict_warning ( $record_num , & $conflicts )
{
$this -> warnings [ $record_num ] = lang ( 'Conflicts' ) . ':' ;
foreach ( $conflicts as $conflict )
{
$this -> warnings [ $record_num ] .= " <br /> \n " . Api\DateTime :: to ( $conflict [ 'start' ]) . " \t " . $conflict [ 'title' ];
}
}
2010-10-13 19:24:32 +02:00
/**
* returns translated name of plugin
*
* @ return string name
*/
public static function get_name () {
2011-03-14 21:58:28 +01:00
return lang ( 'Calendar CSV import' );
2010-10-13 19:24:32 +02:00
}
/**
* returns translated ( user ) description of plugin
*
* @ return string descriprion
*/
public static function get_description () {
2018-03-26 16:02:42 +02:00
return lang ( " Imports events into your Calendar from a CSV File. CSV means 'Comma Separated Values'. However in the options Tab you can also choose other seperators. " );
2010-10-13 19:24:32 +02:00
}
/**
* retruns file suffix ( s ) plugin can handle ( e . g . csv )
*
* @ return string suffix ( comma seperated )
*/
public static function get_filesuffix () {
return 'csv' ;
}
/**
2015-09-22 19:26:36 +02:00
* Alter a row for preview to show multiple participants instead of Array
2010-10-13 19:24:32 +02:00
*
2015-09-22 19:26:36 +02:00
* @ param egw_record $row_entry
2010-10-13 19:24:32 +02:00
*/
2015-09-22 19:26:36 +02:00
protected function row_preview ( importexport_iface_egw_record & $row_entry )
{
2023-01-16 23:31:25 +01:00
$row_entry -> participants = implode ( '<br />' , $this -> bo -> participants ( array ( 'participants' => $row_entry -> participants ), true ));
2010-10-13 19:24:32 +02:00
}
2023-01-16 23:31:25 +01:00
/**
* return etemplate components for options .
* @ abstract We can ' t deal with etemplate objects here , as an uietemplate
* objects itself are scipt orientated and not " dialog objects "
*
* @ return array (
* name => string ,
* content => array ,
* sel_options => array ,
* preserv => array ,
* )
*/
public function get_options_etpl ( importexport_definition & $definition = null )
{
$owner = $definition -> plugin_options [ 'owner' ] ? ? $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
// Make sure Owner from import dialog is not array
if ( is_array ( $owner ))
{
$owner = array_pop ( $owner );
}
$options = array (
'name' => 'calendar.import_csv' ,
'content' => array (
'owner' => $owner ? $owner : null
)
);
return $options ;
}
2016-05-01 19:47:59 +02:00
}