Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
CRAP
58.14% covered (warning)
58.14%
50 / 86
CargoICalendarFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
157.36
58.14% covered (warning)
58.14%
50 / 86
 allowedParameters
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 queryAndDisplay
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 14
 getCalendar
0.00% covered (danger)
0.00%
0 / 1
9.49
81.82% covered (warning)
81.82%
18 / 22
 getEvent
0.00% covered (danger)
0.00%
0 / 1
15.05
94.12% covered (success)
94.12%
32 / 34
 text
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 wrap
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 8
 esc
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
<?php
/**
 * @ingroup Cargo
 * @file
 */
use MediaWiki\MediaWikiServices;
/**
 * Handle the iCalendar export format.
 * @since 2.6
 */
class CargoICalendarFormat extends CargoDeferredFormat {
    public static function allowedParameters() {
        return [
            'link text' => [ 'type' => 'string' ],
            'filename' => [ 'type' => 'string' ],
            'icalendar name' => [ 'type' => 'string' ],
            'icalendar description' => [ 'type' => 'string' ],
        ];
    }
    /**
     * @param CargoSQLQuery[] $sqlQueries
     * @param string[] $displayParams Unused
     * @param string[]|null $querySpecificParams Unused
     * @return string An HTML link to Special:CargoExport with the required query string.
     */
    public function queryAndDisplay( $sqlQueries, $displayParams, $querySpecificParams = null ) {
        $queryParams = $this->sqlQueriesToQueryParams( $sqlQueries );
        $queryParams['format'] = 'icalendar';
        // Calendar name.
        if ( isset( $displayParams['icalendar name'] ) && $displayParams['icalendar name'] ) {
            $queryParams['icalendar name'] = $displayParams['icalendar name'];
        }
        // Calendar description.
        if ( isset( $displayParams['icalendar description'] )
            && $displayParams['icalendar description']
        ) {
            $queryParams['icalendar description'] = $displayParams['icalendar description'];
        }
        // Filename.
        if ( isset( $displayParams['filename'] ) && $displayParams['filename'] ) {
            $queryParams['filename'] = $displayParams['filename'];
        }
        // Link.
        if ( isset( $displayParams['link text'] ) && $displayParams['link text'] ) {
            $linkText = $displayParams['link text'];
        } else {
            $linkText = wfMessage( 'cargo-viewicalendar' )->parse();
        }
        $export = SpecialPage::getTitleFor( 'CargoExport' );
        return Html::rawElement( 'a', [ 'href' => $export->getFullURL( $queryParams ) ], $linkText );
    }
    /**
     * Get the iCalendar format output.
     * @param WebRequest $request
     * @param CargoSQLQuery[] $sqlQueries
     * @return string
     */
    public function getCalendar( WebRequest $request, $sqlQueries ) {
        $name = $request->getText( 'icalendar_name', 'Calendar' );
        $desc = $request->getText( 'icalendar_description', false );
        // Merge, remove empty lines, and re-index the lines of the calendar.
        $calLines = array_values( array_filter( array_merge(
            [
                'BEGIN:VCALENDAR',
                'VERSION:2.0',
                'PRODID:mediawiki/cargo',
            ],
            $this->text( 'NAME', $name ),
            $this->text( 'X-WR-CALNAME', $name ),
            $desc ? $this->text( 'DESCRIPTION', $desc ) : []
        ) ) );
        foreach ( $sqlQueries as $sqlQuery ) {
            list( $startDateField, $endDateField ) = $sqlQuery->getMainStartAndEndDateFields();
            // @todo - get rid of this "if" check; right now it's only needed
            // to pass validation, for some strange reason.
            if ( $startDateField == null ) {
                $startDateField = 'start';
            }
            $queryResults = $sqlQuery->run();
            $nameField = '_pageName';
            if ( !array_key_exists( '_pageName', $queryResults[0] ) ) {
                foreach ( array_keys( $queryResults[0] ) as $resField ) {
                    if ( ( $resField !== $startDateField ) && ( $resField !== $endDateField ) ) {
                        $nameField = $resField;
                        break;
                    }
                }
            }
            foreach ( $queryResults as $result ) {
                $eventLines = $this->getEvent( $result, $startDateField, $endDateField, $nameField );
                $calLines = array_merge( $calLines, $eventLines );
            }
        }
        $calLines[] = 'END:VCALENDAR';
        return implode( "\r\n", $calLines );
    }
    /**
     * Get the lines of an event.
     * @param string[] $result
     * @return string[]
     */
    public function getEvent( $result, $startDateField, $endDateField, $nameField ) {
        $title = Title::newFromText( $result[$nameField] );
        // Only re-query the Page if its ID or modification date are not included in the original query.
        if ( !isset( $result['_pageID'] ) || !isset( $result['_modificationDate'] ) ) {
            $page = CargoUtils::makeWikiPage( $title );
        }
        $pageId = isset( $result['_pageID'] ) ? $result['_pageID'] : $page->getId();
        $permalink = SpecialPage::getTitleFor( 'Redirect', 'page/' . $pageId, $result['_ID'] ?? '' );
        $uid = $permalink->getCanonicalURL();
        // Page values are stored in the wiki's timezone.
        $wikiTimezone = MediaWikiServices::getInstance()->getMainConfig()->get( 'Localtimezone' );
        if ( isset( $result[$startDateField] ) ) {
            $startDateTime = new DateTime( $result[$startDateField], new DateTimeZone( $wikiTimezone ) );
            $start = wfTimestamp( TS_ISO_8601_BASIC, $startDateTime->getTimestamp() );
        } else {
            $start = false;
        }
        if ( $endDateField !== null && isset( $result[$endDateField] ) ) {
            $endDateTime = new DateTime( $result[$endDateField], new DateTimeZone( $wikiTimezone ) );
            $end = wfTimestamp( TS_ISO_8601_BASIC, $endDateTime->getTimestamp() );
        } else {
            $end = false;
        }
        // Modification date is stored in UTC.
        $dtstamp = isset( $result['_modificationDate'] )
            ? wfTimestamp( TS_ISO_8601_BASIC, $result['_modificationDate'] )
            : MWTimestamp::convert( TS_ISO_8601_BASIC, $page->getTimestamp() );
        $desc = false;
        if ( isset( $result['description'] ) && $result['description'] ) {
            $desc = $result['description'];
        }
        $location = false;
        if ( isset( $result['location'] ) && $result['location'] ) {
            $location = $result['location'];
        }
        // Merge, remove empty lines, and re-index the lines of the event.
        return array_values( array_filter( array_merge(
            [
                'BEGIN:VEVENT',
                'UID:' . $uid,
                'DTSTAMP:' . $dtstamp,
            ],
            $this->text( 'SUMMARY', $title->getText() ),
            [
                'DTSTART:' . $start,
                $end ? 'DTEND:' . $end : '',
            ],
            $desc ? $this->text( 'DESCRIPTION', $desc ) : [],
            $location ? $this->text( 'LOCATION', $location ) : [],
            [
                'END:VEVENT',
            ]
        ) ) );
    }
    /**
     * Get the lines of a text property (wrapped and escaped).
     * @param string $prop The text property name.
     * @param string $str The value of the property.
     * @return string[]
     */
    public function text( $prop, $str ) {
        $lang = CargoUtils::getContentLang()->getHtmlCode();
        // Make sure the language conforms to RFC5646.
        $langProp = '';
        if ( Language::isWellFormedLanguageTag( $lang ) ) {
            $langProp = ";LANGUAGE=$lang";
        }
        return $this->wrap( "$prop$langProp:" . $this->esc( $str ) );
    }
    /**
     * Wrap a line into an array of strings with max length 75 bytes.
     *
     * Kudos to  spatie/icalendar-generator, MIT license.
     * @link https://github.com/spatie/icalendar-generator/blob/00346196cf526de2ae3e4ccc562294a59a27b5b2/src/Builders/ComponentBuilder.php#L76..L92
     *
     * @param string $line
     * @return string[]
     */
    public function wrap( $line ) {
        $chippedLines = [];
        while ( strlen( $line ) > 0 ) {
            if ( strlen( $line ) > 75 ) {
                $chippedLines[] = mb_strcut( $line, 0, 75, 'utf-8' );
                $line = ' ' . mb_strcut( $line, 75, strlen( $line ), 'utf-8' );
            } else {
                $chippedLines[] = $line;
                break;
            }
        }
        return $chippedLines;
    }
    /**
     * Escape a string according to RFC5545 (backslashes, semicolons, commas, and newlines).
     * @link https://tools.ietf.org/html/rfc5545#section-3.3.11
     * @param string $str
     * @return string
     */
    public function esc( $str ) {
        $replacements = [
            '\\' => '\\\\',
            ';' => '\\;',
            ',' => '\\,',
            "\n" => '\\n',
        ];
        return str_replace( array_keys( $replacements ), $replacements, $str );
    }
}