Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.44% covered (success)
94.44%
17 / 18
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Utils
94.44% covered (success)
94.44%
17 / 18
75.00% covered (warning)
75.00%
3 / 4
10.02
0.00% covered (danger)
0.00%
0 / 1
 getWikiIDString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 guessStringDirection
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 timezoneToUserTimeCorrection
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 getAnswerAggregationTimestamp
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare( strict_types=1 );
4
5namespace MediaWiki\Extension\CampaignEvents;
6
7use DateTime;
8use DateTimeZone;
9use MediaWiki\DAO\WikiAwareEntity;
10use MediaWiki\Extension\CampaignEvents\Event\ExistingEventRegistration;
11use MediaWiki\Extension\CampaignEvents\Participants\Participant;
12use MediaWiki\Extension\CampaignEvents\Questions\EventAggregatedAnswersStore;
13use MediaWiki\User\UserTimeCorrection;
14use MediaWiki\Utils\MWTimestamp;
15use MediaWiki\WikiMap\WikiMap;
16
17/**
18 * Simple utility methods.
19 */
20class Utils {
21    public const VIRTUAL_DB_DOMAIN = 'virtual-campaignevents';
22
23    /**
24     * @param string|false $wikiID
25     * @return string
26     */
27    public static function getWikiIDString( $wikiID ): string {
28        return $wikiID !== WikiAwareEntity::LOCAL ? $wikiID : WikiMap::getCurrentWikiId();
29    }
30
31    /**
32     * Guesses the direction of a string, e.g. an address, for use in the "dir" attribute.
33     *
34     * @param string $address
35     * @return string Either 'ltr' or 'rtl'
36     */
37    public static function guessStringDirection( string $address ): string {
38        // Taken from https://stackoverflow.com/a/48918886/7369689
39        // TODO: There should really be a nicer way to do this.
40        $rtlRe = '/[\x{0590}-\x{083F}]|[\x{08A0}-\x{08FF}]|[\x{FB1D}-\x{FDFF}]|[\x{FE70}-\x{FEFF}]/u';
41        return preg_match( $rtlRe, $address ) ? 'rtl' : 'ltr';
42    }
43
44    /**
45     * @internal
46     * Converts a DateTimeZone object into a UserTimeCorrection object.
47     * This logic could perhaps be moved to UserTimeCorrection in the future.
48     *
49     * @param DateTimeZone $tz
50     * @return UserTimeCorrection
51     */
52    public static function timezoneToUserTimeCorrection( DateTimeZone $tz ): UserTimeCorrection {
53        // Timezones in PHP can be either a geographical zone ("Europe/Rome"), an offset ("+01:00"), or
54        // an abbreviation ("GMT"). PHP provides no way to tell which format a timezone object uses.
55        // DateTimeZone seems to have an internal timezone_type property but it's set magically and inaccessible.
56        // Also, 'UTC' is surprisingly categorized as a geographical zone, and getLocation() does not return false
57        // for it, but rather an array with incomplete data. PHP, WTF?!
58        $timezoneName = $tz->getName();
59        if ( strpos( $timezoneName, '/' ) !== false ) {
60            // Geographical format, convert to the format used by UserTimeCorrection.
61            $minDiff = floor( $tz->getOffset( new DateTime() ) / 60 );
62            return new UserTimeCorrection( "ZoneInfo|$minDiff|$timezoneName" );
63        }
64        if ( preg_match( '/^[+-]\d{2}:\d{2}$/', $timezoneName ) ) {
65            // Offset, which UserTimeCorrection accepts directly.
66            return new UserTimeCorrection( $timezoneName );
67        }
68        // Non-geographical named zone. Convert to offset because UserTimeCorrection only accepts
69        // the other two types. In theory, this conversion shouldn't change the absolute time and it should
70        // not depend on DST, because abbreviations already contain information about DST (e.g., "PST" vs "PDT").
71        // TODO This assumption may be false for some abbreviations, see T316688#8336443.
72
73        // Work around PHP bug: all versions of PHP up to 7.4.x, 8.0.20 and 8.1.7 do not parse DST correctly for
74        // time zone abbreviations, and PHP assumes that *all* abbreviations correspond to time zones without DST.
75        // So we can't use DateTimeZone::getOffset(), and the timezone must also be specified inside the time string,
76        // and not as second argument to DateTime::__construct. See https://bugs.php.net/bug.php?id=74671
77        $randomTime = '2022-10-20 18:00:00 ' . $timezoneName;
78        $offset = ( new DateTime( $randomTime ) )->format( 'P' );
79        return new UserTimeCorrection( $offset );
80    }
81
82    /**
83     * Given a participant and an event registration, returns the timestamp when the answers of this participant should
84     * be aggregated. This method may return a timestamp in the past (e.g., if it's recent enough that we still haven't
85     * been able to aggregate the answers), but it will return null if the participant never answered any questions, or
86     * if their answers have already been aggregated. In particular, this means that if the answers have already been
87     * aggregated, the aggregation timestamp is ignored. This is motivated by the current UI, where participant whose
88     * answers have been aggregated are treated the same as those who never answered any question.
89     *
90     * @param Participant $participant
91     * @param ExistingEventRegistration $event
92     * @return string|null Timestamp in TS_UNIX format
93     */
94    public static function getAnswerAggregationTimestamp(
95        Participant $participant,
96        ExistingEventRegistration $event
97    ): ?string {
98        $firstAnswerTime = $participant->getFirstAnswerTimestamp();
99        if ( $firstAnswerTime === null || $participant->getAggregationTimestamp() !== null ) {
100            return null;
101        }
102        $participantAggregationTS = (int)$firstAnswerTime + EventAggregatedAnswersStore::ANSWERS_TTL_SEC;
103        $eventAggregationTS = (int)MWTimestamp::convert( TS_UNIX, $event->getEndUTCTimestamp() );
104        return (string)min( $participantAggregationTS, $eventAggregationTS );
105    }
106}