-
Notifications
You must be signed in to change notification settings - Fork 244
Commit
Signed-off-by: SebastianKrupinski <[email protected]>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<?php | ||
/** | ||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
namespace OCA\Calendar\Command; | ||
|
||
use InvalidArgumentException; | ||
use OCA\Calendar\Service\Export\ExportService; | ||
use OCP\Calendar\ICalendarExport; | ||
use OCP\Calendar\IManager; | ||
use OCP\IUserManager; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputArgument; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
|
||
class Export extends Command { | ||
public function __construct( | ||
private IUserManager $userManager, | ||
private IManager $calendarManager, | ||
private ExportService $exportService, | ||
) { | ||
parent::__construct(); | ||
} | ||
|
||
protected function configure(): void { | ||
$this->setName('calendar:export') | ||
->setDescription('Export a specific calendar for a user') | ||
->addArgument('uid', InputArgument::REQUIRED, 'Id of system user') | ||
->addArgument('cid', InputArgument::REQUIRED, 'Id of calendar') | ||
->addArgument('format', InputArgument::OPTIONAL, 'Format of output (iCal, jCal, xCal) default to iCal') | ||
->addArgument('location', InputArgument::OPTIONAL, 'location of where to write the output. defaults to stdout'); | ||
} | ||
|
||
protected function execute(InputInterface $input, OutputInterface $output): int { | ||
|
||
$userId = $input->getArgument('uid'); | ||
$calendarId = $input->getArgument('cid'); | ||
$format = $input->getArgument('format'); | ||
$location = $input->getArgument('location'); | ||
|
||
if (!$this->userManager->userExists($userId)) { | ||
throw new InvalidArgumentException("User <$userId> not found."); | ||
} | ||
// retrieve calendar and evaluate if export is supported | ||
$calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]); | ||
if ($calendars === []) { | ||
throw new InvalidArgumentException("Calendar <$calendarId> not found."); | ||
} | ||
$calendar = $calendars[0]; | ||
if (!$calendar instanceof ICalendarExport) { | ||
Check failure on line 52 in lib/Command/Export.php GitHub Actions / static-psalm-analysis dev-stable30UndefinedClass
Check failure on line 52 in lib/Command/Export.php GitHub Actions / static-psalm-analysis dev-stable31UndefinedClass
|
||
throw new InvalidArgumentException("Calendar <$calendarId> dose support this function"); | ||
} | ||
// evaluate if requested format is supported | ||
if ($format !== null && !in_array($format, $this->exportService::FORMATS)) { | ||
throw new InvalidArgumentException("Format <$format> is not valid."); | ||
} elseif ($format === null) { | ||
$format = 'ical'; | ||
} | ||
// evaluate is a valid location was given and is usable otherwise output to stdout | ||
if ($location !== null) { | ||
$handle = fopen($location, "w"); | ||
if ($handle === false) { | ||
throw new InvalidArgumentException("Location <$location> is not valid. Can not open location for write operation."); | ||
} else { | ||
foreach ($this->exportService->export($calendar, $format) as $chunk) { | ||
fwrite($handle, $chunk); | ||
} | ||
fclose($handle); | ||
} | ||
} else { | ||
foreach ($this->exportService->export($calendar, $format) as $chunk) { | ||
$output->writeln($chunk); | ||
} | ||
} | ||
|
||
return self::SUCCESS; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
<?php | ||
/** | ||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
namespace OCA\Calendar\Command; | ||
|
||
use InvalidArgumentException; | ||
use OCA\Calendar\Service\Import\ImportService; | ||
use OCP\Calendar\CalendarImportSettings; | ||
use OCP\Calendar\ICalendarImport; | ||
use OCP\Calendar\IManager; | ||
use OCP\IUserManager; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputArgument; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
|
||
class Import extends Command { | ||
public function __construct( | ||
private IUserManager $userManager, | ||
private IManager $calendarManager, | ||
private ImportService $importService, | ||
) { | ||
parent::__construct(); | ||
} | ||
|
||
protected function configure(): void { | ||
$this->setName('calendar:import') | ||
->setDescription('Import a file or stream') | ||
->addArgument('uid', InputArgument::REQUIRED, 'Id of system user') | ||
->addArgument('cid', InputArgument::REQUIRED, 'Id of calendar') | ||
->addArgument('format', InputArgument::OPTIONAL, 'Format of output (iCal, jCal, xCal) default to iCal') | ||
->addArgument('location', InputArgument::OPTIONAL, 'location of where to write the output. defaults to stdin'); | ||
} | ||
|
||
protected function execute(InputInterface $input, OutputInterface $output): int { | ||
|
||
$userId = $input->getArgument('uid'); | ||
$calendarId = $input->getArgument('cid'); | ||
$format = $input->getArgument('format'); | ||
$location = $input->getArgument('location'); | ||
|
||
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 ICalendarImport) { | ||
Check failure on line 53 in lib/Command/Import.php GitHub Actions / static-psalm-analysis dev-stable30UndefinedClass
Check failure on line 53 in lib/Command/Import.php GitHub Actions / static-psalm-analysis dev-stable31UndefinedClass
|
||
throw new InvalidArgumentException("Calendar <$calendarId> dose 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 settings object | ||
$settings = new CalendarImportSettings(); | ||
// evaluate if provided format is supported | ||
if ($format !== null && !in_array($format, $this->importService::FORMATS)) { | ||
throw new \InvalidArgumentException("Format <$format> is not valid."); | ||
} else { | ||
$settings->format = $format ?? 'ical'; | ||
} | ||
// evaluate if a valid location was given and is usable otherwise default to stdin | ||
if ($location !== null) { | ||
$input = fopen($location, "r"); | ||
if ($input === false) { | ||
throw new \InvalidArgumentException("Location <$location> is not valid. Can not open location for read operation."); | ||
} else { | ||
try { | ||
$outcome = $this->importService->import($input, $calendar, $settings); | ||
} finally { | ||
fclose($input); | ||
} | ||
} | ||
} else { | ||
$input = fopen('php://stdin', 'r'); | ||
if ($input === false) { | ||
throw new \InvalidArgumentException("Can not open stdin for read operation."); | ||
} else { | ||
try { | ||
$temp = tmpfile(); | ||
while (!feof($input)) { | ||
fwrite($temp, fread($input, 8192)); | ||
} | ||
fseek($temp, 0); | ||
$outcome = $this->importService->import($temp, $calendar, $settings); | ||
} finally { | ||
fclose($input); | ||
fclose($temp); | ||
} | ||
} | ||
} | ||
|
||
return self::SUCCESS; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/** | ||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
namespace OCA\Calendar\Controller; | ||
|
||
use OCA\Calendar\AppInfo\Application; | ||
use OCA\Calendar\Service\Export\ExportService; | ||
use OCP\AppFramework\Controller; | ||
use OCP\AppFramework\Http; | ||
use OCP\AppFramework\Http\Attribute\ApiRoute; | ||
use OCP\AppFramework\Http\Attribute\NoAdminRequired; | ||
use OCP\AppFramework\Http\Attribute\UserRateLimit; | ||
use OCP\AppFramework\Http\DataResponse; | ||
use OCP\AppFramework\Http\StreamGeneratorResponse; | ||
use OCP\Calendar\ICalendarExport; | ||
use OCP\Calendar\IManager; | ||
use OCP\IGroupManager; | ||
use OCP\IRequest; | ||
use OCP\IUserManager; | ||
use OCP\IUserSession; | ||
|
||
class ExportController extends Controller { | ||
|
||
public function __construct( | ||
IRequest $request, | ||
private IUserSession $userSession, | ||
private IUserManager $userManager, | ||
private IGroupManager $groupManager, | ||
private IManager $calendarManager, | ||
private ExportService $exportService | ||
) { | ||
parent::__construct(Application::APP_ID, $request); | ||
} | ||
|
||
#[ApiRoute(verb: 'GET', url: '/export', root: '/calendar')] | ||
#[ApiRoute(verb: 'POST', url: '/export', root: '/calendar')] | ||
#[UserRateLimit(limit: 1, period: 60)] | ||
#[NoAdminRequired] | ||
public function index(string $id, ?string $fmt = null, ?string $user = null) { | ||
|
||
$userId = $user; | ||
$calendarId = $id; | ||
$format = $fmt; | ||
// evaluate if user is logged in and has permissions | ||
if (!$this->userSession->isLoggedIn()) { | ||
return new DataResponse([], Http::STATUS_UNAUTHORIZED); | ||
} | ||
if ($userId !== null) { | ||
if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID()) && | ||
$this->userSession->getUser()->getUID() !== $userId) { | ||
return new DataResponse([], Http::STATUS_UNAUTHORIZED); | ||
} | ||
if (!$this->userManager->userExists($userId)) { | ||
return new DataResponse(['error' => 'user not found'], Http::STATUS_BAD_REQUEST); | ||
} | ||
} else { | ||
$userId = $this->userSession->getUser()->getUID(); | ||
} | ||
// retrieve calendar and evaluate if export is supported | ||
$calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]); | ||
if ($calendars === []) { | ||
return new DataResponse(['error' => 'calendar not found'], Http::STATUS_BAD_REQUEST); | ||
} | ||
$calendar = $calendars[0]; | ||
/* | ||
if ($calendar instanceof ICalendarExport) { | ||
return new DataResponse(['error' => 'calendar export not supported'], Http::STATUS_BAD_REQUEST); | ||
} | ||
*/ | ||
// evaluate if requested format is supported and convert to output content type | ||
if ($format !== null && !in_array($format, $this->exportService::FORMATS)) { | ||
return new DataResponse(['error' => 'format invalid'], Http::STATUS_BAD_REQUEST); | ||
} elseif ($format === null) { | ||
$format = 'ical'; | ||
} | ||
$contentType = match (strtolower($format)) { | ||
'jcal' => 'application/calendar+json; charset=UTF-8', | ||
'xcal' => 'application/calendar+xml; charset=UTF-8', | ||
default => 'text/calendar; charset=UTF-8' | ||
}; | ||
|
||
return new StreamGeneratorResponse($this->exportService->export($calendar, $format), $contentType); | ||
Check failure on line 87 in lib/Controller/ExportController.php GitHub Actions / static-psalm-analysis dev-stable30UndefinedClass
Check failure on line 87 in lib/Controller/ExportController.php GitHub Actions / static-psalm-analysis dev-stable30InvalidArgument
Check failure on line 87 in lib/Controller/ExportController.php GitHub Actions / static-psalm-analysis dev-stable31UndefinedClass
Check failure on line 87 in lib/Controller/ExportController.php GitHub Actions / static-psalm-analysis dev-stable31InvalidArgument
|
||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/** | ||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
namespace OCA\Calendar\Service\Export; | ||
|
||
use Generator; | ||
use OCP\Calendar\CalendarExportRange; | ||
use OCP\Calendar\ICalendar; | ||
use OCP\Calendar\ICalendarExport; | ||
use Sabre\VObject\Component; | ||
use Sabre\VObject\Writer; | ||
|
||
class ExportService { | ||
|
||
public const FORMATS = ['ical', 'jcal', 'xcal']; | ||
|
||
public function __construct() {} | ||
|
||
/** | ||
* Retrieves calendar object, converts each object to appropriate format and streams output | ||
*/ | ||
public function export(ICalendar&ICalendarExport $calendar, string $format, ?CalendarExportRange $range = null): Generator { | ||
Check failure on line 27 in lib/Service/Export/ExportService.php GitHub Actions / static-psalm-analysis dev-stable30UndefinedClass
Check failure on line 27 in lib/Service/Export/ExportService.php GitHub Actions / static-psalm-analysis dev-stable30UndefinedClass
Check failure on line 27 in lib/Service/Export/ExportService.php GitHub Actions / static-psalm-analysis dev-stable31UndefinedClass
Check failure on line 27 in lib/Service/Export/ExportService.php GitHub Actions / static-psalm-analysis dev-stable31UndefinedClass
|
||
|
||
yield $this->exportStart($format); | ||
|
||
// iterate through each returned vCalendar entry | ||
// extract each component except timezones, convert to appropriate format and output | ||
// extract any timezones and save them but do not output | ||
$timezones = []; | ||
foreach ($calendar->export($range) as $entry) { | ||
$consecutive = false; | ||
foreach ($entry->getComponents() as $vComponent) { | ||
if ($vComponent->name === 'VTIMEZONE') { | ||
if (isset($vComponent->TZID) && !isset($timezones[$vComponent->TZID->getValue()])) { | ||
$timezones[$vComponent->TZID->getValue()] = clone $vComponent; | ||
} | ||
} else { | ||
yield $this->exportObject($vComponent, $format, $consecutive); | ||
$consecutive = true; | ||
} | ||
} | ||
} | ||
// iterate through each vTimezone entry, convert to appropriate format and output | ||
foreach ($timezones as $vComponent) { | ||
yield $this->exportObject($vComponent, $format, $consecutive); | ||
$consecutive = true; | ||
} | ||
|
||
yield $this->exportFinish($format); | ||
|
||
} | ||
|
||
/** | ||
* Generates appropriate output start based on selected format | ||
*/ | ||
private function exportStart(string $format): string { | ||
return match ($format) { | ||
'jcal' => '["vcalendar",[["version",{},"text","2.0"],["prodid",{},"text","-\/\/IDN nextcloud.com\/\/Calendar App\/\/EN"]],[', | ||
'xcal' => '<?xml version="1.0" encoding="UTF-8"?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><version><text>2.0</text></version><prodid><text>-//IDN nextcloud.com//Calendar App//EN</text></prodid></properties><components>', | ||
default => "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//IDN nextcloud.com//Calendar App//EN\n" | ||
}; | ||
} | ||
|
||
/** | ||
* Generates appropriate output end based on selected format | ||
*/ | ||
private function exportFinish(string $format): string { | ||
return match ($format) { | ||
'jcal' => ']]', | ||
'xcal' => '</components></vcalendar></icalendar>', | ||
default => "END:VCALENDAR\n" | ||
}; | ||
} | ||
|
||
/** | ||
* Generates appropriate output content for a component based on selected format | ||
*/ | ||
private function exportObject(Component $vobject, string $format, bool $consecutive): string { | ||
Check failure on line 83 in lib/Service/Export/ExportService.php GitHub Actions / static-psalm-analysis dev-stable30UndefinedClass
Check failure on line 83 in lib/Service/Export/ExportService.php GitHub Actions / static-psalm-analysis dev-stable31UndefinedClass
|
||
return match ($format) { | ||
'jcal' => $consecutive ? ',' . Writer::writeJson($vobject) : Writer::writeJson($vobject), | ||
'xcal' => $this->exportObjectXml($vobject), | ||
default => Writer::write($vobject) | ||
}; | ||
} | ||
|
||
/** | ||
* Generates appropriate output content for a component for xml format | ||
*/ | ||
private function exportObjectXml(Component $vobject): string { | ||
Check failure on line 94 in lib/Service/Export/ExportService.php GitHub Actions / static-psalm-analysis dev-stable30UndefinedClass
Check failure on line 94 in lib/Service/Export/ExportService.php GitHub Actions / static-psalm-analysis dev-stable31UndefinedClass
|
||
$writer = new \Sabre\Xml\Writer(); | ||
$writer->openMemory(); | ||
$writer->setIndent(false); | ||
$vobject->xmlSerialize($writer); | ||
return $writer->outputMemory(); | ||
} | ||
|
||
} |