2025-04-03 20:27:49 -04:00
< ? php
declare ( strict_types = 1 );
/**
* SPDX - FileCopyrightText : 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
*/
namespace OCA\DAV\Command ;
use InvalidArgumentException ;
use OCA\DAV\CalDAV\CalendarImpl ;
use OCA\DAV\CalDAV\Import\ImportService ;
use OCP\Calendar\CalendarImportOptions ;
use OCP\Calendar\IManager ;
use OCP\ITempManager ;
use OCP\IUserManager ;
use Symfony\Component\Console\Attribute\AsCommand ;
use Symfony\Component\Console\Command\Command ;
use Symfony\Component\Console\Input\InputArgument ;
use Symfony\Component\Console\Input\InputInterface ;
use Symfony\Component\Console\Input\InputOption ;
use Symfony\Component\Console\Output\OutputInterface ;
/**
* Calendar Import Command
*
* Used to import data to supported calendars from disk or stdin
*/
#[AsCommand(
name : 'calendar:import' ,
description : 'Import calendar data to supported calendars from disk or stdin' ,
hidden : false
)]
class ImportCalendar extends Command {
public function __construct (
private IUserManager $userManager ,
private IManager $calendarManager ,
private ITempManager $tempManager ,
private ImportService $importService ,
) {
parent :: __construct ();
}
protected function configure () : void {
$this -> setName ( 'calendar:import' )
-> setDescription ( 'Import calendar data to supported calendars from disk or stdin' )
-> addArgument ( 'uid' , InputArgument :: REQUIRED , 'Id of system user' )
2026-01-22 10:22:29 -05:00
-> addArgument ( 'uri' , InputArgument :: REQUIRED , 'URI of calendar' )
2025-04-03 20:27:49 -04:00
-> addArgument ( 'location' , InputArgument :: OPTIONAL , 'Location to read the input from, defaults to stdin.' )
-> addOption ( 'format' , null , InputOption :: VALUE_REQUIRED , 'Format of input (ical, jcal, xcal) defaults to ical' , 'ical' )
2026-01-22 10:22:29 -05:00
-> addOption ( 'errors' , null , InputOption :: VALUE_REQUIRED , 'how to handle item errors (0 - continue, 1 - fail)' )
-> addOption ( 'validation' , null , InputOption :: VALUE_REQUIRED , 'how to handle item validation (0 - no validation, 1 - validate and skip on issue, 2 - validate and fail on issue)' )
2025-04-03 20:27:49 -04:00
-> addOption ( 'supersede' , null , InputOption :: VALUE_NONE , 'override/replace existing items' )
-> addOption ( 'show-created' , null , InputOption :: VALUE_NONE , 'show all created items after processing' )
-> addOption ( 'show-updated' , null , InputOption :: VALUE_NONE , 'show all updated items after processing' )
-> addOption ( 'show-skipped' , null , InputOption :: VALUE_NONE , 'show all skipped items after processing' )
-> addOption ( 'show-errors' , null , InputOption :: VALUE_NONE , 'show all errored items after processing' );
}
protected function execute ( InputInterface $input , OutputInterface $output ) : int {
$userId = $input -> getArgument ( 'uid' );
$calendarId = $input -> getArgument ( 'uri' );
$location = $input -> getArgument ( 'location' );
$format = $input -> getOption ( 'format' );
$errors = is_numeric ( $input -> getOption ( 'errors' )) ? ( int ) $input -> getOption ( 'errors' ) : null ;
$validation = is_numeric ( $input -> getOption ( 'validation' )) ? ( int ) $input -> getOption ( 'validation' ) : null ;
$supersede = $input -> getOption ( 'supersede' );
$showCreated = $input -> getOption ( 'show-created' );
$showUpdated = $input -> getOption ( 'show-updated' );
$showSkipped = $input -> getOption ( 'show-skipped' );
$showErrors = $input -> getOption ( 'show-errors' );
if ( ! $this -> userManager -> userExists ( $userId )) {
throw new InvalidArgumentException ( " User < $userId > not found. " );
}
// retrieve calendar and evaluate if import is supported and writeable
$calendars = $this -> calendarManager -> getCalendarsForPrincipal ( 'principals/users/' . $userId , [ $calendarId ]);
if ( $calendars === []) {
throw new InvalidArgumentException ( " Calendar < $calendarId > not found " );
}
$calendar = $calendars [ 0 ];
if ( ! $calendar instanceof CalendarImpl ) {
throw new InvalidArgumentException ( " Calendar < $calendarId > doesn't support this function " );
}
if ( ! $calendar -> isWritable ()) {
throw new InvalidArgumentException ( " Calendar < $calendarId > is not writeable " );
}
if ( $calendar -> isDeleted ()) {
throw new InvalidArgumentException ( " Calendar < $calendarId > is deleted " );
}
// construct options object
$options = new CalendarImportOptions ();
$options -> setSupersede ( $supersede );
if ( $errors !== null ) {
$options -> setErrors ( $errors );
}
if ( $validation !== null ) {
$options -> setValidate ( $validation );
}
$options -> setFormat ( $format );
// evaluate if a valid location was given and is usable otherwise default to stdin
$timeStarted = microtime ( true );
if ( $location !== null ) {
$input = fopen ( $location , 'r' );
if ( $input === false ) {
2026-01-22 10:22:29 -05:00
throw new InvalidArgumentException ( " Location < $location > is not valid. Cannot open location for read operation. " );
2025-04-03 20:27:49 -04:00
}
try {
$outcome = $this -> importService -> import ( $input , $calendar , $options );
} finally {
fclose ( $input );
}
} else {
$input = fopen ( 'php://stdin' , 'r' );
if ( $input === false ) {
2026-01-22 10:22:29 -05:00
throw new InvalidArgumentException ( 'Cannot open stdin for read operation.' );
2025-04-03 20:27:49 -04:00
}
try {
$tempPath = $this -> tempManager -> getTemporaryFile ();
$tempFile = fopen ( $tempPath , 'w+' );
while ( ! feof ( $input )) {
fwrite ( $tempFile , fread ( $input , 8192 ));
}
fseek ( $tempFile , 0 );
$outcome = $this -> importService -> import ( $tempFile , $calendar , $options );
} finally {
fclose ( $input );
fclose ( $tempFile );
}
}
$timeFinished = microtime ( true );
// summarize the outcome
$totalCreated = 0 ;
$totalUpdated = 0 ;
$totalSkipped = 0 ;
$totalErrors = 0 ;
if ( $outcome !== []) {
if ( $showCreated || $showUpdated || $showSkipped || $showErrors ) {
$output -> writeln ( '' );
}
foreach ( $outcome as $id => $result ) {
if ( isset ( $result [ 'outcome' ])) {
switch ( $result [ 'outcome' ]) {
case 'created' :
$totalCreated ++ ;
if ( $showCreated ) {
$output -> writeln ([ 'created: ' . $id ]);
}
break ;
case 'updated' :
$totalUpdated ++ ;
if ( $showUpdated ) {
$output -> writeln ([ 'updated: ' . $id ]);
}
break ;
case 'exists' :
$totalSkipped ++ ;
if ( $showSkipped ) {
$output -> writeln ([ 'skipped: ' . $id ]);
}
break ;
case 'error' :
$totalErrors ++ ;
if ( $showErrors ) {
$output -> writeln ([ 'errors: ' . $id ]);
$output -> writeln ( $result [ 'errors' ]);
}
break ;
}
}
}
}
$output -> writeln ([
'' ,
'Import Completed' ,
'================' ,
'Execution Time: ' . ( $timeFinished - $timeStarted ) . ' sec' ,
'Total Created: ' . $totalCreated ,
'Total Updated: ' . $totalUpdated ,
'Total Skipped: ' . $totalSkipped ,
'Total Errors: ' . $totalErrors ,
''
]);
return self :: SUCCESS ;
}
}