Files
Git'Fellow 4920bd1725 refactor: add PHP 8.3 types to BC-safe class constants
PHP 8.3 allows class constants to declare a type. Adopt it where doing so
cannot break third-party apps.

Typing an inheritable constant is a hard BC break: a subclass that
redeclares it untyped fails to load with "Type of C::FOO must be
compatible with P::FOO of type string". Changes are therefore limited to
constants that cannot be redeclared by a subclass:

  - private const (not inherited)
  - final public/protected const
  - constants declared in a final class or an enum

Interface constants, trait constants, and public/protected constants in
non-final (including abstract) classes are left untyped, as is all of
lib/public (OCP) and lib/unstable (NCU).

One review-requested exception: the public OBJECT_PREFIX/OBJECT_SUFFIX
constants of the app-internal CalDAV import helpers (TextImporter,
XmlImporter) are typed as well; these classes are not public API and
have no subclasses.

Only string, int and array are used. float is avoided because it would
silently coerce an int literal and change === comparisons. No constant
name or value is modified: every changed line adds only the type token,
so runtime behaviour is unchanged.

Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2026-08-11 13:52:52 +01:00

158 lines
4.5 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Import;
use Exception;
class TextImporter {
public const string OBJECT_PREFIX = 'BEGIN:VCALENDAR' . PHP_EOL;
public const string OBJECT_SUFFIX = PHP_EOL . 'END:VCALENDAR';
private const array COMPONENT_TYPES = ['VEVENT', 'VTODO', 'VJOURNAL', 'VTIMEZONE'];
private bool $analyzed = false;
private array $structure = ['VCALENDAR' => [], 'VEVENT' => [], 'VTODO' => [], 'VJOURNAL' => [], 'VTIMEZONE' => []];
/**
* @param resource $source
*/
public function __construct(
private $source,
) {
// Ensure that source is a stream resource
if (!is_resource($source) || get_resource_type($source) !== 'stream') {
throw new Exception('Source must be a stream resource');
}
}
/**
* Analyzes the source data and creates a structure of components
*/
private function analyze() {
$componentStart = null;
$componentEnd = null;
$componentId = null;
$componentType = null;
$tagName = null;
$tagValue = null;
// iterate through the source data line by line
fseek($this->source, 0);
while (!feof($this->source)) {
$data = fgets($this->source);
// skip empty lines
if ($data === false || empty(trim($data))) {
continue;
}
// lines with whitespace at the beginning are continuations of the previous line
if (ctype_space($data[0]) === false) {
// detect the line TAG
// detect the first occurrence of ':' or ';'
$colonPos = strpos($data, ':');
$semicolonPos = strpos($data, ';');
if ($colonPos !== false && $semicolonPos !== false) {
$splitPosition = min($colonPos, $semicolonPos);
} elseif ($colonPos !== false) {
$splitPosition = $colonPos;
} elseif ($semicolonPos !== false) {
$splitPosition = $semicolonPos;
} else {
continue;
}
$tagName = strtoupper(trim(substr($data, 0, $splitPosition)));
$tagValue = trim(substr($data, $splitPosition + 1));
$tagContinuation = false;
} else {
$tagContinuation = true;
$tagValue .= trim($data);
}
if ($tagContinuation === false) {
// check line for component start, remember the position and determine the type
if ($tagName === 'BEGIN' && in_array($tagValue, self::COMPONENT_TYPES, true)) {
$componentStart = ftell($this->source) - strlen($data);
$componentType = $tagValue;
}
// check line for component end, remember the position
if ($tagName === 'END' && $componentType === $tagValue) {
$componentEnd = ftell($this->source);
}
// check line for component id
if ($componentStart !== null && ($tagName === 'UID' || $tagName === 'TZID')) {
$componentId = $tagValue;
}
} else {
// check line for component id
if ($componentStart !== null && ($tagName === 'UID' || $tagName === 'TZID')) {
$componentId = $tagValue;
}
}
// any line(s) not inside a component are VCALENDAR properties
if ($componentStart === null) {
if ($tagName !== 'BEGIN' && $tagName !== 'END' && $tagValue === 'VCALENDAR') {
$components['VCALENDAR'][] = $data;
}
}
// if component start and end are found, add the component to the structure
if ($componentStart !== null && $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;
}
}
}
/**
* Returns the analyzed structure of the source data
* the analyzed structure is a collection of components organized by type,
* each entry is a collection of instances
* [
* 'VEVENT' => [
* '7456f141-b478-4cb9-8efc-1427ba0d6839' => [
* ['VEVENT', '7456f141-b478-4cb9-8efc-1427ba0d6839', 0, 100 ],
* ['VEVENT', '7456f141-b478-4cb9-8efc-1427ba0d6839', 100, 200 ]
* ]
* ]
* ]
*/
public function structure(): array {
if (!$this->analyzed) {
$this->analyze();
}
return $this->structure;
}
/**
* Extracts a string chuck from the source data
*
* @param int $start starting byte position
* @param int $end ending byte position
*/
public function extract(int $start, int $end): string {
fseek($this->source, $start);
return fread($this->source, $end - $start);
}
}