'Lines not delimited by CRLF sequence near line # 1

I'm having an issue getting the line folding to work the way it's specified. I'm obviously misunderstanding something about the documentation, so I was hoping I could get some help. The validator at https://icalendar.org/validator.html is saying

Lines not delimited by CRLF sequence near line # 1
Reference: RFC 5545 3.1. Content Lines

This is my function to generate the .ics files for download:

public function getCalendarFile($event) {
    header("Content-Type: text/Calendar; charset=utf-8");
    header("Content-Disposition: attachment; filename="ExampleFile.ics");
    $icsFile = "BEGIN:VCALENDAR\r\n";
    $icsFile .= "VERSION:2.0\r\n";
    $icsFile .= "PRODID:Example Event" . $event->name . "\r\n";
    $icsFile .= "METHOD:PUBLISH\r\n";
    $icsFile .= "BEGIN:VEVENT\r\n";
    $icsFile .= "UID:". $event->name . gmdate("Ymd\THis\Z", strtotime(Carbon::now())) . "\r\n";
    $icsFile .= "DTSTAMP:" . gmdate("Ymd\THis\Z", strtotime(Carbon::now())) . "\r\n";
    $icsFile .= "DTSTART:" . gmdate("Ymd\THis\Z", strtotime($event->begin)) . "\r\n";
    $icsFile .= "DTEND:" . gmdate("Ymd\THis\Z", strtotime($event->end)) . "\r\n";
    $icsFile .= "LOCATION:" . strip_tags($event->location) . "\r\n";
    $icsFile .= "SUMMARY:" . $event->name . "\r\n";
    $icsFile .= "DESCRIPTION:" . $this->foldCalendarDescription(strip_tags($event->description)) . "\r\n";
    $icsFile .= "END:VEVENT\r\n";
    $icsFile .= "END:VCALENDAR\r\n";
    echo $icsFile;
}

public function foldCalendarDescription($description) {
    return wordwrap($description, 75, "\r\n\t", TRUE);
}

I'm not sure if it has something to do with strip_tags possibly? The event description is stored as a wysiwyg html input. But the issue says with line # 1 and line # 1 looks fine to me.



Solution 1:[1]

Here's a wrapper for ICAL strings that works for me:

function format_ical_string( $s ) {
    $r = wordwrap(
        preg_replace(
            array( '/,/', '/;/', '/[\r\n]/' ),
            array( '\,', '\;', '\n' ),
            $s
        ), 73, "\n", TRUE
    );

    // Indent all lines but first:
    $r = preg_replace( '/\n/', "\n  ", $r );

    return $r;
}

Solution 2:[2]

As I wrote in the comment beneath kmoser's answer, people seem to be really hard pressed to get exactly 75 bytes on a line and create very convoluted code to do that. But why not just sometimes less than 75 bytes? A simple wordwrap will give you that because it just looks for whitespace to break and is not multi-byte aware. You'll possibly have a few more linebreaks in the iCal object code than strictly neccessary, but does that matter? This is why I upvoted kmoser's answer. It's a nice and simple solution.

I've tried to create an even simpler and faster version of kmoser's answer:

function format_ical_string( $str ) {

    $str = str_replace(
        [ "\r\n", '\\', ',', ';', "\n" ], // replacement order is important
        [ "\n", '\\\\', '\,', '\;', '\n' ], $str );

    return wordwrap( $str, 73, "\n ", false );
}

The replacements (just a str_replace, because nothing fancy is needed here) are:

  1. replace CRLF's with just LF's
  2. replace literal backslashes with escaped backslashes
  3. escape comma's
  4. escape semicolons
  5. replace newlines with a literal \n (per rfc5545)

Then wordwrap with a LF + space (per rfc5545).

After this, strictly you should replace all LF's with CRLF's (rfc5545 line ending). I tend to format the entire iCal object with just LF's, and only at the very end replace all LF's with CRLF's to make the result compliant. This saves me the hassle of repeatedly inserting these Windows line endings (who uses those these days?) during the composition of my iCal objects.

It is unclear from rfc5545 if the property name itself is counted in the 75 bytes line limit, as in https://www.rfc-editor.org/rfc/rfc5545#section-3.1:

Lines of text SHOULD NOT be longer than 75 octets, excluding the line break.

But just to be sure, I feed the property name to the function, as in:

$line = format_ical_string( 'SUMMARY:Really long text here' );

IMPORTANT: my function above will fail on single words (!) that are longer than 75 bytes. I'm not sure how common that is. If you have an 18-character word completely made of 4-byte characters, you'd be at the limit. This seems very unlikely to me.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 kmoser
Solution 2