setTimezone( $site_tz ); return array( (int) $local->format( 'Ymd' ), (int) $local->format( 'His' ) ); } /** * Whether a legacy entry row is all-day or untimed (literal dates, no rebase). * * Heuristic per Phase 0: untimed rows store cal_time = -1; all-day rows * store cal_time = 0 with cal_duration = 1440. A genuinely timed midnight * event has cal_time = 0 with a different duration. * * @param array $row Legacy webcal_entry row. */ public static function is_all_day( array $row ): bool { $time = (int) ( $row['cal_time'] ?? -1 ); $duration = (int) ( $row['cal_duration'] ?? 0 ); return -1 === $time || ( 0 === $time && 1440 === $duration ); } /** * Applies the timezone/all-day rules to a legacy webcal_entry row. * * @param array $row Legacy webcal_entry row. * @return array Row with rebased date/time values. */ public static function transform_entry_times( array $row, DateTimeZone $site_tz ): array { if ( self::is_all_day( $row ) ) { // Literal dates stay; collapse both legacy encodings to the // plugin's all-day representation. $row['cal_time'] = '-1'; return $row; } list( $date, $time ) = self::rebase_to_site_time( (int) $row['cal_date'], (int) $row['cal_time'], $site_tz ); $row['cal_date'] = (string) $date; $row['cal_time'] = (string) $time; if ( isset( $row['cal_due_date'], $row['cal_due_time'] ) && (int) $row['cal_due_time'] >= 0 ) { list( $due_date, $due_time ) = self::rebase_to_site_time( (int) $row['cal_due_date'], (int) $row['cal_due_time'], $site_tz ); $row['cal_due_date'] = (string) $due_date; $row['cal_due_time'] = (string) $due_time; } if ( isset( $row['cal_mod_date'], $row['cal_mod_time'] ) ) { list( $mod_date, $mod_time ) = self::rebase_to_site_time( (int) $row['cal_mod_date'], (int) $row['cal_mod_time'], $site_tz ); $row['cal_mod_date'] = (string) $mod_date; $row['cal_mod_time'] = (string) $mod_time; } return $row; } /** * Converts the pre-1.9 Sunday-first cal_days mask ('ynynynn') to a * cal_byday list ('SU,TU,TH'), mirroring legacy's own upgrade logic. * All-yes and all-no masks mean "no day selection" and yield ''. */ public static function cal_days_to_byday( string $mask ): string { if ( 'yyyyyyy' === $mask || 'nnnnnnn' === $mask || 7 !== strlen( $mask ) ) { return ''; } $days = array(); foreach ( self::CAL_DAYS_ORDER as $i => $abbrev ) { if ( 'y' === $mask[ $i ] ) { $days[] = $abbrev; } } return implode( ',', $days ); } /** * Maps repeat type names retired by legacy's own upgrades onto their * modern equivalents (dumps from never-upgraded databases carry them). */ public static function normalize_repeat_type( string $type ): string { return 'monthlyByDayR' === $type ? 'monthlyByDay' : $type; } /** * Keep only valid RFC 5545 BYDAY tokens from a legacy cal_byday value. * * Legacy dumps occasionally carry junk in this field (e.g. "TH,-17"), which * builds an unparseable RRULE (`BYDAY=TH,-17`) that then 500s the calendar * feed. Each token must be an optional ±ordinal followed by a weekday * (SU/MO/TU/WE/TH/FR/SA); anything else is dropped. * * @param string $byday Comma-separated legacy BYDAY value. * @return string Sanitized comma-separated BYDAY value ('' if none valid). */ public static function sanitize_byday( string $byday ): string { $valid = array(); foreach ( explode( ',', $byday ) as $token ) { $token = trim( $token ); if ( '' !== $token && 1 === preg_match( '/^[+-]?[0-9]{0,2}(SU|MO|TU|WE|TH|FR|SA)$/', $token ) ) { $valid[] = $token; } } return implode( ',', $valid ); } /** * Keep only integer tokens from a legacy cal_bysetpos value. * * @param string $bysetpos Comma-separated legacy BYSETPOS value. * @return string Sanitized comma-separated integer list ('' if none valid). */ public static function sanitize_bysetpos( string $bysetpos ): string { $valid = array(); foreach ( explode( ',', $bysetpos ) as $token ) { $token = trim( $token ); if ( '' !== $token && 1 === preg_match( '/^-?[0-9]+$/', $token ) ) { $valid[] = $token; } } return implode( ',', $valid ); } /** * Deterministic UID matching legacy's own iCal-export format, so that a * calendar previously imported from a legacy .ics export UID-matches * instead of duplicating. * * Mirrors generate_uid() in legacy includes/xcal.php: server URL without * scheme, ':' to '-', '--<10-digit id>' appended, runs of '/' and * '-' collapsed to a single '-'. */ public static function legacy_uid( int $legacy_id, string $server_url, string $login ): string { $uid = '' !== $server_url ? $server_url : 'UNCONFIGURED-WEBCALENDAR'; $uid = str_replace( array( 'http://', 'https://' ), '', $uid ); $uid = str_replace( ':', '-', $uid ); $uid .= sprintf( '-%s-%010d', $login, $legacy_id ); return (string) preg_replace( '/[\/-]+/', '-', $uid ); } }