| @@ -288,8 +288,81 @@ | ||
| 288 | 288 | $level -= 1; |
| 289 | 289 | return $html; |
| 290 | 290 | } |
| 291 | 291 | |
| 292 | + | |
| 293 | + // FixIn: 2025-08-11. 12:52 | |
| 294 | + /** | |
| 295 | + * Find a safe byte index to cut $s at, not exceeding $maxBytes. | |
| 296 | + * - Prefer last ' ' or ',' or ';' <= $maxBytes | |
| 297 | + * - Avoid cutting immediately after a backslash (to keep escapes like "\n" intact) | |
| 298 | + * - Ensure we don't split a UTF-8 code point | |
| 299 | + * Returns byte length to keep. | |
| 300 | + */ | |
| 301 | + private static function _utf8_safe_cut_pos( $s, $maxBytes ) { | |
| 302 | + $len = strlen( $s ); | |
| 303 | + if ( $len <= $maxBytes ) { | |
| 304 | + return $len; | |
| 305 | + } | |
| 306 | + | |
| 307 | + // Start with hard limit | |
| 308 | + $cut = $maxBytes; | |
| 309 | + | |
| 310 | + // Prefer last breakable char within the current window | |
| 311 | + $window = substr( $s, 0, $cut ); | |
| 312 | + $lastSpace = strrpos( $window, ' ' ); | |
| 313 | + $lastComma = strrpos( $window, ',' ); | |
| 314 | + $lastSemi = strrpos( $window, ';' ); | |
| 315 | + $pref = max( $lastSpace === false ? - 1 : $lastSpace, $lastComma === false ? - 1 | |
| 316 | + : $lastComma, $lastSemi === false ? - 1 : $lastSemi ); | |
| 317 | + | |
| 318 | + if ( $pref >= 0 ) { | |
| 319 | + $cut = $pref + 1; // cut *after* the delimiter/space so it stays on line | |
| 320 | + } | |
| 321 | + | |
| 322 | + // Avoid cutting right after backslash (don’t break "\n", "\,", "\;", "\\") | |
| 323 | + if ( $cut > 0 && $s[ $cut - 1 ] === '\\' ) { | |
| 324 | + $cut -= 1; | |
| 325 | + if ( $cut <= 0 ) { | |
| 326 | + $cut = $maxBytes; | |
| 327 | + } // fallback | |
| 328 | + } | |
| 329 | + | |
| 330 | + // Ensure we don't chop inside a UTF-8 multibyte char | |
| 331 | + // Move back until at start of a UTF-8 code point | |
| 332 | + while ( $cut > 0 && ( ord( $s[ $cut ] ) & 0xC0 ) === 0x80 ) { | |
| 333 | + $cut --; | |
| 334 | + } | |
| 335 | + if ( $cut <= 0 ) { | |
| 336 | + $cut = $maxBytes; | |
| 337 | + } // ultimate fallback | |
| 338 | + | |
| 339 | + return $cut; | |
| 340 | + } | |
| 341 | + | |
| 342 | + /** | |
| 343 | + * Fold one logical line into RFC 5545 physical lines. | |
| 344 | + * $firstWidth = 75 octets, $contWidth = 74 octets. | |
| 345 | + */ | |
| 346 | + private static function _fold_ical_line( $line, $firstWidth = 75, $contWidth = 74 ) { | |
| 347 | + $out = ''; | |
| 348 | + $first = true; | |
| 349 | + while ( strlen( $line ) > 0 ) { | |
| 350 | + $width = $first ? $firstWidth : $contWidth; | |
| 351 | + $cut = self::_utf8_safe_cut_pos( $line, $width ); | |
| 352 | + $chunk = substr( $line, 0, $cut ); | |
| 353 | + $line = substr( $line, $cut ); | |
| 354 | + if ( ! $first ) { | |
| 355 | + $out .= ' '; | |
| 356 | + } | |
| 357 | + $out .= $chunk . "\r\n"; | |
| 358 | + $first = false; | |
| 359 | + } | |
| 360 | + | |
| 361 | + return $out; | |
| 362 | + } | |
| 363 | + | |
| 364 | + | |
| 292 | 365 | /** |
| 293 | 366 | * export tree to icalendar format |
| 294 | 367 | * |
| 295 | 368 | * @param object $node Top level node to export |
| @@ -317,27 +390,15 @@ | ||
| 317 | 390 | $p .= ";" . strtoupper($key) . "=" . $value; |
| 318 | 391 | } |
| 319 | 392 | } |
| 320 | 393 | $values = $d->getValues(); |
| 321 | - // don't think we need this, Sunbird does not like it in the EXDATE field | |
| 322 | - //$values = str_replace(",", "\\,", $values); | |
| 323 | 394 | |
| 324 | 395 | $line = $d->getName() . $p . ":" . $values; |
| 325 | - $line = str_replace(array("<br>","<BR>","<br/>","<BR/"),"\\n",$line); | |
| 326 | - $line = str_replace(array("\r\n","\n\r","\n","\r"),'\n',$line); | |
| 327 | - //$line = str_replace(array(',',';','\\'), array('\\,','\\;','\\\\'),$line); | |
| 328 | - //$line =strip_tags($line); | |
| 329 | - $linecount = 0; | |
| 330 | - while (strlen($line) > 0) { | |
| 331 | - $linewidth = ($linecount == 0? 75 : 74); | |
| 332 | - $linesize = (strlen($line) > $linewidth? $linewidth: strlen($line)); | |
| 333 | - if($linecount > 0) | |
| 334 | - $txtstr .= " "; | |
| 335 | - $txtstr .= substr($line,0,$linesize) . "\r\n"; | |
| 336 | - $linecount += 1; | |
| 337 | - $line = substr($line,$linewidth); | |
| 338 | - } | |
| 339 | - //echo $line . "\n"; | |
| 396 | + $line = str_replace( array( "<br>", "<BR>", "<br/>", "<BR/" ), "\\n", $line ); | |
| 397 | + $line = str_replace( array( "\r\n", "\n\r", "\n", "\r" ), '\n', $line ); | |
| 398 | + | |
| 399 | + // FixIn: 2025-08-11 12:53. | |
| 400 | + $txtstr .= self::_fold_ical_line( $line ); | |
| 340 | 401 | } |
| 341 | 402 | foreach ($node->child as $c){ |
| 342 | 403 | $txtstr .= $node->export($c,$level + 1); |
| 343 | 404 | } |