-
Notifications
You must be signed in to change notification settings - Fork 241
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,161 @@ | ||
<?php | ||
/** | ||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
namespace OCA\Calendar\Command; | ||
|
||
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) { | ||
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 { | ||
|
||
fwrite($handle, "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//IDN nextcloud.com//Calendar App//EN\n"); | ||
|
||
|
||
for ($i=0; $i < 16384; $i++) { | ||
|
||
$id = uniqid(); | ||
fwrite( | ||
$handle, | ||
'BEGIN:VEVENT | ||
CREATED:20240910T123608Z | ||
LAST-MODIFIED:20240916T075225Z | ||
DTSTAMP:20240916T075225Z | ||
UID:' . $id . PHP_EOL . | ||
'SUMMARY:Brainstorming workshop- Collectives - Room A | ||
STATUS:CONFIRMED | ||
ORGANIZER;CN=Irina Mikhaylina;SCHEDULE-STATUS=1.1:mailto:irina.mikhaylina@n | ||
extcloud.com | ||
ATTENDEE;RSVP=TRUE;CN=Jonas Meurer;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL; | ||
ROLE=REQ-PARTICIPANT;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:jonas.meurer@n | ||
extcloud.com | ||
ATTENDEE;RSVP=TRUE;CN=Simon Lindner;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL | ||
;ROLE=REQ-PARTICIPANT;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:simon.lindner | ||
@nextcloud.com | ||
ATTENDEE;RSVP=TRUE;CN=Louis Chemineau;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDU | ||
AL;ROLE=REQ-PARTICIPANT;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:louis.chemi | ||
[email protected] | ||
ATTENDEE;RSVP=TRUE;CN=Jenna Stocks;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL; | ||
ROLE=REQ-PARTICIPANT;LANGUAGE=en_GB;SCHEDULE-STATUS=1.1:mailto:jenna.stock | ||
[email protected] | ||
ATTENDEE;RSVP=TRUE;CN=Marcel Hibbe;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL; | ||
ROLE=REQ-PARTICIPANT;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:marcel.hibbe@n | ||
extcloud.com | ||
ATTENDEE;RSVP=TRUE;CN=Peter Mocanu;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL; | ||
ROLE=REQ-PARTICIPANT;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:peter.mocanu@n | ||
extcloud.com | ||
ATTENDEE;RSVP=TRUE;CN=Cyprien Edouard;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDU | ||
AL;ROLE=REQ-PARTICIPANT;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:cyprien.edo | ||
[email protected] | ||
ATTENDEE;RSVP=TRUE;CN=Kim Pohlmann;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL; | ||
ROLE=REQ-PARTICIPANT;LANGUAGE=de;SCHEDULE-STATUS=1.1:mailto:kim.pohlmann@n | ||
extcloud.com | ||
ATTENDEE;RSVP=TRUE;CN=Tobias Kaminsky;PARTSTAT=ACCEPTED;CUTYPE=INDIVIDUAL;R | ||
OLE=REQ-PARTICIPANT;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:tobias.kaminsky | ||
@nextcloud.com | ||
DTSTART;TZID=Europe/Berlin:20240919T100000 | ||
DTEND;TZID=Europe/Berlin:20240919T110000 | ||
SEQUENCE:4 | ||
LOCATION:Room A (ground floor) | ||
DESCRIPTION:Hello dear team\, \n \nwe would like to invite you to join ou | ||
r upcoming Product Brainstorming Session\, where we will come together in | ||
a group of 10 people to spark creativity and collaborate on potential new | ||
features for Nextcloud apps 💥 Here is the structure of the session:\n\ | ||
n1. Introduction (5 minutes): We\'ll start with a brief introduction from t | ||
he design team\, followed by participants introducing themselves. To make | ||
things fun and relaxed\, we\'ll choose a moderator from the group. We’ll | ||
just ask for volunteers on the spot\, so no one feels pressured or assigne | ||
d without their agreement.\n\n2. App Demo (10 minutes): Next\, the develop | ||
er of each team will share their screen and give a short demo of the app\, | ||
walking everyone through its current features and explaining what it does | ||
.\n\n3. Idea Generation (10 minutes): After the demo\, it\'s time for every | ||
one to jot down ideas for new features or improvements they\'d like to see. | ||
You can come up with up to 5 ideas and stick them on a whiteboard for us | ||
all to review.\n\n4. Discussion (25 minutes): The moderator will then go t | ||
hrough each idea on the board. The person who wrote it will explain their | ||
thought process\, and then we’ll open the floor for feedback. Everyone c | ||
an share whether they agree\, disagree\, or offer praise and suggestions.\ | ||
n\n5. Prioritization (10 minutes): We’ll pick the most important ideas b | ||
ased on our discussion.\n\nThroughout the session\, the moderator will kee | ||
p track of time and ensure we don’t spend too long on any one idea 🤓 | ||
\n\nIf you have any questions\, feel free to reach out with me!\n\nKind re | ||
gards\nIrina | ||
X-MOZ-GENERATION:1 | ||
END:VEVENT | ||
' | ||
); | ||
} | ||
fwrite($handle, "END:VCALENDAR\n"); | ||
/* | ||
foreach ($this->exportService->export($calendar, $format) as $chunk) { | ||
fwrite($handle, $chunk); | ||
} | ||
*/ | ||
fclose($handle); | ||
} | ||
} else { | ||
foreach ($this->exportService->export($calendar, $format) as $chunk) { | ||
Check failure on line 154 in lib/Command/Export.php GitHub Actions / static-psalm-analysis dev-stable30InvalidArgument
Check failure on line 154 in lib/Command/Export.php GitHub Actions / static-psalm-analysis dev-masterInvalidArgument
|
||
$output->writeln($chunk); | ||
} | ||
} | ||
|
||
return self::SUCCESS; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
<?php | ||
/** | ||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
namespace OCA\Calendar\Command; | ||
|
||
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 52 in lib/Command/Import.php GitHub Actions / static-psalm-analysis dev-stable30UndefinedClass
Check failure on line 52 in lib/Command/Import.php GitHub Actions / static-psalm-analysis dev-masterUndefinedClass
|
||
//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."); | ||
} elseif ($format === null) { | ||
$settings->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 { | ||
$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); | ||
$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-masterUndefinedClass
Check failure on line 87 in lib/Controller/ExportController.php GitHub Actions / static-psalm-analysis dev-masterInvalidArgument
|
||
|
||
} | ||
} |