From c8dc1bef552b639fc001b64410f16b7ca5631bcc Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Sun, 26 Jan 2025 12:16:38 -0500 Subject: [PATCH] fixup! feat: OCC and OCS Calendar Import/Export Signed-off-by: SebastianKrupinski --- lib/Command/Export.php | 95 +-------- lib/Command/Import.php | 21 +- lib/Service/Export/ExportService.php | 11 +- lib/Service/Import/ImportService.php | 284 ++++++++++++--------------- lib/Service/Import/TextImporter.php | 123 ++++++++++++ lib/Service/Import/XmlImporter.php | 168 ++++++++++++++++ 6 files changed, 442 insertions(+), 260 deletions(-) create mode 100644 lib/Service/Import/TextImporter.php create mode 100644 lib/Service/Import/XmlImporter.php diff --git a/lib/Command/Export.php b/lib/Command/Export.php index 7c77e961b..55a4a4f71 100644 --- a/lib/Command/Export.php +++ b/lib/Command/Export.php @@ -5,6 +5,7 @@ */ namespace OCA\Calendar\Command; +use InvalidArgumentException; use OCA\Calendar\Service\Export\ExportService; use OCP\Calendar\ICalendarExport; use OCP\Calendar\IManager; @@ -40,22 +41,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int $location = $input->getArgument('location'); if (!$this->userManager->userExists($userId)) { - throw new \InvalidArgumentException("User <$userId> not found."); + 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."); + throw new InvalidArgumentException("Calendar <$calendarId> not found."); } $calendar = $calendars[0]; - /* - if ($calendar instanceof ICalendarExport) { - throw new \InvalidArgumentException("Calendar <$calendarId> dose support this function"); + 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."); + throw new InvalidArgumentException("Format <$format> is not valid."); } elseif ($format === null) { $format = 'ical'; } @@ -63,91 +62,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int 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."); + 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 - neau@nextcloud.com -ATTENDEE;RSVP=TRUE;CN=Jenna Stocks;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL; - ROLE=REQ-PARTICIPANT;LANGUAGE=en_GB;SCHEDULE-STATUS=1.1:mailto:jenna.stock - s@nextcloud.com -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 - uard@nextcloud.com -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 { diff --git a/lib/Command/Import.php b/lib/Command/Import.php index 4786623fd..0f423cdda 100644 --- a/lib/Command/Import.php +++ b/lib/Command/Import.php @@ -5,6 +5,7 @@ */ namespace OCA\Calendar\Command; +use InvalidArgumentException; use OCA\Calendar\Service\Import\ImportService; use OCP\Calendar\CalendarImportSettings; use OCP\Calendar\ICalendarImport; @@ -41,30 +42,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int $location = $input->getArgument('location'); if (!$this->userManager->userExists($userId)) { - throw new \InvalidArgumentException("User <$userId> not found."); + 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."); + throw new InvalidArgumentException("Calendar <$calendarId> not found."); } $calendar = $calendars[0]; - if ($calendar instanceof ICalendarImport) { - //throw new \InvalidArgumentException("Calendar <$calendarId> dose support this function"); + if (!$calendar instanceof ICalendarImport) { + throw new InvalidArgumentException("Calendar <$calendarId> dose support this function"); } if (!$calendar->isWritable()) { - throw new \InvalidArgumentException("Calendar <$calendarId> is not writeable"); + throw new InvalidArgumentException("Calendar <$calendarId> is not writeable"); } if ($calendar->isDeleted()) { - throw new \InvalidArgumentException("Calendar <$calendarId> is deleted"); + 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'; + } else { + $settings->format = $format ?? 'ical'; } // evaluate if a valid location was given and is usable otherwise default to stdin if ($location !== null) { @@ -73,7 +74,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new \InvalidArgumentException("Location <$location> is not valid. Can not open location for read operation."); } else { try { - $this->importService->import($input, $calendar, $settings); + $outcome = $this->importService->import($input, $calendar, $settings); } finally { fclose($input); } @@ -89,7 +90,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int fwrite($temp, fread($input, 8192)); } fseek($temp, 0); - $this->importService->import($temp, $calendar, $settings); + $outcome = $this->importService->import($temp, $calendar, $settings); } finally { fclose($input); fclose($temp); diff --git a/lib/Service/Export/ExportService.php b/lib/Service/Export/ExportService.php index 473b81459..4af7d84db 100644 --- a/lib/Service/Export/ExportService.php +++ b/lib/Service/Export/ExportService.php @@ -29,20 +29,19 @@ public function export(ICalendar&ICalendarExport $calendar, string $format, ?Cal yield $this->exportStart($format); // iterate through each returned vCalendar entry - // extract each vObject type, convert to appropriate format and output - // extract any vTimezones objects and save them but do not output + // 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') { - yield $this->exportObject($vComponent, $format, $consecutive); - $consecutive = true; - } 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; } } } diff --git a/lib/Service/Import/ImportService.php b/lib/Service/Import/ImportService.php index 0f1c70f5a..8edad8a06 100644 --- a/lib/Service/Import/ImportService.php +++ b/lib/Service/Import/ImportService.php @@ -8,40 +8,40 @@ */ namespace OCA\Calendar\Service\Import; -use Exception; +use Generator; use OCP\Calendar\CalendarImportSettings; use OCP\Calendar\ICalendar; use OCP\Calendar\ICalendarImport; use Sabre\VObject\Component\VCalendar; -use Sabre\VObject\Node; use Sabre\VObject\Reader; +use XMLReader; class ImportService { public const FORMATS = ['ical', 'jcal', 'xcal']; + private $source; + public function __construct() {} /** * Retrieves calendar object, converts each object to appropriate format and streams output */ - public function import($stream, ICalendar $calendar, CalendarImportSettings $settings): void { + public function import($source, ICalendar $calendar, CalendarImportSettings $settings): void { - $time_start = microtime(true); + $this->source = $source; - echo "\nMemory usage: " . memory_get_usage()/1048576; - - //Reader::read($stream); + $time_start = microtime(true); switch ($settings->format) { case 'ical': - $this->importICal($stream, $calendar, $settings); + $calendar->import($settings, $this->importText(...)); break; case 'jcal': - $this->importICal($stream, $calendar, $settings); + $calendar->import($settings, $this->importJson(...)); break; case 'xcal': - $this->importICal($stream, $calendar, $settings); + $calendar->import($settings, $this->importXml(...)); break; } @@ -54,19 +54,20 @@ public function import($stream, ICalendar $calendar, CalendarImportSettings $set } - private function importICal($stream, ICalendar $calendar, CalendarImportSettings $settings): void { + private function importText(CalendarImportSettings $settings): Generator { - $outcome = []; - $structure = $this->analyzeICal($stream); + $importer = new TextImporter($this->source); - $vStart = "BEGIN:VCALENDAR" . PHP_EOL; - $vEnd = PHP_EOL . "END:VCALENDAR"; + $structure = $importer->structure(); + + $sObjectPrefix = $importer::OBJECT_PREFIX; + $sObjectSuffix = $importer::OBJECT_SUFFIX; // calendar properties foreach ($structure['VCALENDAR'] as $entry) { - $vStart .= $entry; + $sObjectPrefix .= $entry; if (!substr($entry, -1) === "\n" || !substr($entry, -2) === "\r\n") { - $vStart .= PHP_EOL; + $sObjectPrefix .= PHP_EOL; } } @@ -74,174 +75,145 @@ private function importICal($stream, ICalendar $calendar, CalendarImportSettings $timezones = []; foreach ($structure['VTIMEZONE'] as $tid => $collection) { $instance = $collection[0]; - $vData = $this->extractICalData($stream, $instance[2], $instance[3]); - $vObject = Reader::read($vStart . $vData . $vEnd); + $sObjectContents = $importer->extract($instance[2], $instance[3]); + $vObject = Reader::read($sObjectPrefix . $sObjectContents . $sObjectSuffix); $timezones[$tid] = clone $vObject->VTIMEZONE; } // calendar components - $components = []; - foreach ($structure['VEVENT'] as $cid => $collection) { - // extract and unserialize components - $vData = ""; - foreach ($collection as $instance) { - $vData .= $this->extractICalData($stream, $instance[2], $instance[3]); - } - /** @var VCalendar $vObject */ - $vObject = Reader::read($vStart . $vData . $vEnd); - // extract all timezones from properties for all instances - $vObjectTZ = []; - foreach ($vObject->getComponents() as $vComponent) { - if ($vComponent->name !== 'VTIMEZONE') { - foreach (['DTSTART', 'DTEND', 'DUE', 'RDATE', 'EXDATE'] as $property) { - if (isset($vComponent->$property?->parameters['TZID'])) { - $tid = $vComponent->$property->parameters['TZID']->getValue(); - if (isset($timezones[$tid]) && !isset($vObjectTZ[$tid])) { - $vObjectTZ[$tid] = clone $timezones[$tid]; + foreach (['VEVENT', 'VTODO', 'VJOURNAL'] as $type) { + foreach ($structure[$type] as $cid => $instances) { + // extract all instances of component and unserialize to object + $sObjectContents = ""; + foreach ($instances as $instance) { + $sObjectContents .= $importer->extract($instance[2], $instance[3]); + } + /** @var VCalendar $vObject */ + $vObject = Reader::read($sObjectPrefix . $sObjectContents . $sObjectSuffix); + // extract all timezones from properties for all instances + $vObjectTZ = []; + foreach ($vObject->getComponents() as $vComponent) { + if ($vComponent->name !== 'VTIMEZONE') { + foreach (['DTSTART', 'DTEND', 'DUE', 'RDATE', 'EXDATE'] as $property) { + if (isset($vComponent->$property?->parameters['TZID'])) { + $tid = $vComponent->$property->parameters['TZID']->getValue(); + if (isset($timezones[$tid]) && !isset($vObjectTZ[$tid])) { + $vObjectTZ[$tid] = clone $timezones[$tid]; + } } } } } - } - // add time zones to object - foreach ($vObjectTZ as $zone) { - $vObject->add($zone); - } - // validate object - if ($settings->validate !== 0) { - $issues = $this->validateComponent($vObject, true, 3); - } else { - $issues = []; - } - if ($settings->validate === 1 && $issues !== []) { - $outcome[$cid] = $issues; - continue; - } - if ($settings->validate === 2 && $issues !== []) { - throw new Exception('Error importing calendar object <' . $cid . '> ' . $issues[0]); - } - // add objects to collection until max batch size is reached - if ($settings->bulk > count($components)) { - $components[] = $vObject; - } - // save collection to storage if max batch size is reached - if ($settings->bulk <= count($components)) { - $calendar->import($settings, ...$components); - $components = []; - } - } - - // save collection to storage if max bulk was not reached - if (count($components) > 0) { - $calendar->import($settings, ...$components); - $components = []; - } - - } + // add time zones to object + foreach ($vObjectTZ as $zone) { + $vObject->add($zone); + } + // return object + yield $vObject; - private function validateComponent(VCalendar $vObject, bool $repair, int $level): array { - // validate component(S) - $issues = $vObject->validate(Node::PROFILE_CALDAV); - // attempt to repair - if ($repair && count($issues) > 0) { - $issues = $vObject->validate(Node::REPAIR); - } - // filter out messages based on level - $result = []; - foreach ($issues as $key => $issue) { - if (isset($issue['level']) && $issue['level'] >= $level) { - $result[] = $issue['message']; } } - - return $result; } - private function analyzeICal($stream): array { + private function importXml(CalendarImportSettings $settings): Generator { - $tags = ['VEVENT', 'VTODO', 'VJOURNAL', 'VTIMEZONE']; + $importer = new XmlImporter($this->source); - $components = ['VCALENDAR' => [], 'VEVENT' => [], 'VTODO' => [], 'VJOURNAL' => [], 'VTIMEZONE' => []]; - $componentStart = null; - $componentEnd = null; - $componentId = null; - $componentType = null; + $structure = $importer->structure(); - fseek($stream, 0); - while(!feof($stream)) { - $data = fgets($stream); + $sObjectPrefix = $importer::OBJECT_PREFIX; + $sObjectSuffix = $importer::OBJECT_SUFFIX; - if ($data === false || empty(trim($data))) { - continue; - } - - if (ctype_space($data[0]) === false) { - - if (str_starts_with($data, 'BEGIN:')) { - $type = trim(substr($data, 6)); - if (in_array($type, $tags)) { - $componentStart = ftell($stream) - strlen($data); - $componentType = $type; - } - unset($type); + // calendar time zones + $timezones = []; + foreach ($structure['VTIMEZONE'] as $tid => $collection) { + $instance = $collection[0]; + $sObjectContents = $importer->extract($instance[2], $instance[3]); + $vObject = Reader::readXml($sObjectPrefix . $sObjectContents . $sObjectSuffix); + $timezones[$tid] = clone $vObject->VTIMEZONE; + } + + // calendar components + foreach (['VEVENT', 'VTODO', 'VJOURNAL'] as $type) { + foreach ($structure[$type] as $cid => $instances) { + // extract all instances of component and unserialize to object + $sObjectContents = ""; + foreach ($instances as $instance) { + $sObjectContents .= $importer->extract($instance[2], $instance[3]); } - - if (str_starts_with($data, 'END:')) { - $type = trim(substr($data, 4)); - if ($componentType === $type) { - $componentEnd = ftell($stream); + /** @var VCalendar $vObject */ + $vObject = Reader::readXml($sObjectPrefix . $sObjectContents . $sObjectSuffix); + // extract all timezones from properties for all instances + $vObjectTZ = []; + foreach ($vObject->getComponents() as $vComponent) { + if ($vComponent->name !== 'VTIMEZONE') { + foreach (['DTSTART', 'DTEND', 'DUE', 'RDATE', 'EXDATE'] as $property) { + if (isset($vComponent->$property?->parameters['TZID'])) { + $tid = $vComponent->$property->parameters['TZID']->getValue(); + if (isset($timezones[$tid]) && !isset($vObjectTZ[$tid])) { + $vObjectTZ[$tid] = clone $timezones[$tid]; + } + } + } } - unset($type); } - - if ($componentStart !== null && str_starts_with($data, 'UID:')) { - $componentId = trim(substr($data, 5)); + // add time zones to object + foreach ($vObjectTZ as $zone) { + $vObject->add($zone); } + // return object + yield $vObject; - if ($componentStart !== null && str_starts_with($data, 'TZID:')) { - $componentId = trim(substr($data, 5)); - } - } + } - if ($componentStart === null) { - if (!str_starts_with($data, 'BEGIN:VCALENDAR') && !str_starts_with($data, 'END:VCALENDAR')) { - $components['VCALENDAR'][] = $data; - } - } + } - if ($componentEnd !== null) { - if ($componentId !== null) { - $components[$componentType][$componentId][] = [ - $componentType, - $componentId, - $componentStart, - $componentEnd - ]; - } else { - $components[$componentType][] = [ - $componentType, - $componentId, - $componentStart, - $componentEnd - ]; - } - $componentId = null; - $componentType = null; - $componentStart = null; - $componentEnd = null; + private function importJson(CalendarImportSettings $settings): Generator { + + $importer = Reader::readJson($this->source); + + // calendar time zones + $timezones = []; + foreach ($importer->VTIMEZONE as $timezone) { + $tzid = $timezone->TZID?->getValue(); + if ($tzid !== null) { + $timezones[$tzid] = clone $timezone; } - } - - return $components; - } - private function extractICalData($stream, int $start, $end): string { - - fseek($stream, $start); - return fread($stream, $end - $start); + // calendar components + foreach ($importer->getBaseComponents() as $base) { + /** @var VCalendar $vObject */ + $vObject = new VCalendar; + $vObject->VERSION = clone $importer->VERSION; + $vObject->PRODID = clone $importer->PRODID; + // extract all instances of component + foreach ($importer->getByUID($base->UID->getValue()) as $instance) { + $vObject->add(clone $instance); + } + // extract all timezones from properties for all instances + $vObjectTZ = []; + foreach ($vObject->getComponents() as $vComponent) { + if ($vComponent->name !== 'VTIMEZONE') { + foreach (['DTSTART', 'DTEND', 'DUE', 'RDATE', 'EXDATE'] as $property) { + if (isset($vComponent->$property?->parameters['TZID'])) { + $tid = $vComponent->$property->parameters['TZID']->getValue(); + if (isset($timezones[$tid]) && !isset($vObjectTZ[$tid])) { + $vObjectTZ[$tid] = clone $timezones[$tid]; + } + } + } + } + } + // add time zones to object + foreach ($vObjectTZ as $zone) { + $vObject->add($zone); + } + // return object + yield $vObject; + } } } diff --git a/lib/Service/Import/TextImporter.php b/lib/Service/Import/TextImporter.php new file mode 100644 index 000000000..6aa480da7 --- /dev/null +++ b/lib/Service/Import/TextImporter.php @@ -0,0 +1,123 @@ + [], 'VEVENT' => [], 'VTODO' => [], 'VJOURNAL' => [], 'VTIMEZONE' => []]; + + public function __construct( + protected $source + ) { + //Ensure that the $data var is of the right type + if (!is_string($source) && (!is_resource($source) || get_resource_type($source) !== 'stream')) { + throw new Exception('Source must be a string or a stream resource'); + } + } + + protected function analyze() { + + $componentStart = null; + $componentEnd = null; + $componentId = null; + $componentType = null; + + fseek($this->source, 0); + while(!feof($this->source)) { + $data = fgets($this->source); + + if ($data === false || empty(trim($data))) { + continue; + } + + if (ctype_space($data[0]) === false) { + + if (str_starts_with($data, 'BEGIN:')) { + $type = trim(substr($data, 6)); + if (in_array($type, $this->types)) { + $componentStart = ftell($this->source) - strlen($data); + $componentType = $type; + } + unset($type); + } + + if (str_starts_with($data, 'END:')) { + $type = trim(substr($data, 4)); + if ($componentType === $type) { + $componentEnd = ftell($this->source); + } + unset($type); + } + + if ($componentStart !== null && str_starts_with($data, 'UID:')) { + $componentId = trim(substr($data, 5)); + } + + if ($componentStart !== null && str_starts_with($data, 'TZID:')) { + $componentId = trim(substr($data, 5)); + } + + } + + if ($componentStart === null) { + if (!str_starts_with($data, 'BEGIN:VCALENDAR') && !str_starts_with($data, 'END:VCALENDAR')) { + $components['VCALENDAR'][] = $data; + } + } + + if ($componentEnd !== null) { + if ($componentId !== null) { + $this->structure[$componentType][$componentId][] = [ + $componentType, + $componentId, + $componentStart, + $componentEnd + ]; + } else { + $this->structure[$componentType][] = [ + $componentType, + $componentId, + $componentStart, + $componentEnd + ]; + } + $componentId = null; + $componentType = null; + $componentStart = null; + $componentEnd = null; + } + + } + + } + + public function structure(): array { + + if (!$this->analyzed) { + $this->analyze(); + } + + return $this->structure; + } + + public function extract(int $start, int $end): string { + + fseek($this->source, $start); + return fread($this->source, $end - $start); + + } + +} diff --git a/lib/Service/Import/XmlImporter.php b/lib/Service/Import/XmlImporter.php new file mode 100644 index 000000000..b8e82b319 --- /dev/null +++ b/lib/Service/Import/XmlImporter.php @@ -0,0 +1,168 @@ +'; + public const OBJECT_SUFFIX = ''; + + protected array $types = ['VEVENT', 'VTODO', 'VJOURNAL', 'VTIMEZONE']; + protected bool $analyzed = false; + protected array $structure = ['VCALENDAR' => [], 'VEVENT' => [], 'VTODO' => [], 'VJOURNAL' => [], 'VTIMEZONE' => []]; + protected int $praseLevel = 0; + protected array $prasePath = []; + protected ?int $componentStart = null; + protected ?int $componentEnd = null; + protected int $componentLevel = 0; + protected ?string $componentId = null; + protected ?string $componentType = null; + protected bool $componentIdProperty = false; + + + public function __construct( + protected $source + ) { + //Ensure that the $data var is of the right type + if (!is_string($source) && (!is_resource($source) || get_resource_type($source) !== 'stream')) { + throw new Exception('Source must be a string or a stream resource'); + } + } + + protected function analyze() { + + $this->praseLevel = 0; + $this->prasePath = []; + $this->componentStart = null; + $this->componentEnd = null; + $this->componentLevel = 0; + $this->componentId = null; + $this->componentType = null; + $this->componentIdProperty = false; + //Create the parser + $parser = xml_parser_create(); + // assign handlers + xml_set_object($parser, $this); + xml_set_element_handler($parser, $this->tagStart(...), $this->tagEnd(...)); + xml_set_default_handler($parser, $this->tagContents(...)); + //If the data is a resource then loop through it, otherwise just parse the string + if (is_resource($this->source)) { + //Not all resources support fseek. For those that don't, suppress the error + @fseek($this->source, 0); + while ($chunk = fread($this->source, 4096)) { + if (!xml_parse($parser, $chunk, feof($this->source))) { + throw new Exception( + xml_error_string(xml_get_error_code($parser)) + .' At line: '. + xml_get_current_line_number($parser) + ); + } + } + } else { + if (!xml_parse($parser, $this->source, true)) { + throw new Exception( + xml_error_string(xml_get_error_code($parser)) + .' At line: '. + xml_get_current_line_number($parser) + ); + } + } + + //Free up the parser + xml_parser_free($parser); + + } + + protected function tagStart($parser, $tag, $attributes) { + + $this->praseLevel++; + $this->prasePath[$this->praseLevel] = $tag; + + if (in_array($tag, $this->types)) { + $this->componentStart = xml_get_current_byte_index($parser) - (strlen($tag) + 1); + $this->componentType = $tag; + $this->componentLevel = $this->praseLevel; + } + + if ($this->componentStart !== null && + ($this->componentLevel + 2) === $this->praseLevel && + ($tag === 'UID' || $tag === 'TZID') + ) { + $this->componentIdProperty = true; + } + + return $parser; + } + + protected function tagEnd($parser, $tag) { + + if ($tag === 'UID' || $tag === 'TZID') { + $this->componentIdProperty = false; + } elseif ($this->componentType === $tag) { + $this->componentEnd = xml_get_current_byte_index($parser); + + if ($this->componentId !== null) { + $this->structure[$this->componentType][$this->componentId][] = [ + $this->componentType, + $this->componentId, + $this->componentStart, + $this->componentEnd, + implode('/', $this->prasePath) + ]; + } else { + $this->structure[$this->componentType][] = [ + $this->componentType, + $this->componentId, + $this->componentStart, + $this->componentEnd, + implode('/', $this->prasePath) + ]; + } + $this->componentStart = null; + $this->componentEnd = null; + $this->componentId = null; + $this->componentType = null; + $this->componentIdProperty = false; + } + + unset($this->prasePath[$this->praseLevel]); + $this->praseLevel--; + + return $parser; + } + + protected function tagContents($parser, $data) { + + if ($this->componentIdProperty) { + $this->componentId = $data; + } + + return $parser; + } + + + public function structure(): array { + + if (!$this->analyzed) { + $this->analyze(); + } + + return $this->structure; + } + + public function extract(int $start, int $end): string { + + fseek($this->source, $start); + return fread($this->source, $end - $start); + + } + +}