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        $rtlRe = '/[\x{0590}-\x{083F}]|[\x{08A0}-\x{08FF}]|[\x{FB1D}-\x{FDFF}]|[\x{FE70}-\x{FEFF}]/u';
40        return preg_match( $rtlRe, $address ) ? 'rtl' : 'ltr';
41    }
42
43    /**
44     * @internal
45     * Converts a DateTimeZone object into a UserTimeCorrection object.
46     * This logic could perhaps be moved to UserTimeCorrection in the future.
47     *
48     * @param DateTimeZone $tz
49     * @return UserTimeCorrection
50     */
51    public static function timezoneToUserTimeCorrection( DateTimeZone $tz ): UserTimeCorrection {
52        // Timezones in PHP can be either a geographical zone ("Europe/Rome"), an offset ("+01:00"), or
53        // an abbreviation ("GMT"). PHP provides no way to tell which format a timezone object uses.
54        // DateTimeZone seems to have an internal timezone_type property but it's set magically and inaccessible.
55        // Also, 'UTC' is surprisingly categorized as a geographical zone, and getLocation() does not return false
56        // for it, but rather an array with incomplete data. PHP, WTF?!
57        $timezoneName = $tz->getName();
58        if ( strpos( $timezoneName, '/' ) !== false ) {
59            // Geographical format, convert to the format used by UserTimeCorrection.
60            $minDiff = floor( $tz->getOffset( new DateTime() ) / 60 );
61            return new UserTimeCorrection( "ZoneInfo|$minDiff|$timezoneName" );
62        }
63        if ( preg_match( '/^[+-]\d{2}:\d{2}$/', $timezoneName ) ) {
64            // Offset, which UserTimeCorrection accepts directly.
65            return new UserTimeCorrection( $timezoneName );
66        }
67        // Non-geographical named zone. Convert to offset because UserTimeCorrection only accepts
68        // the other two types. In theory, this conversion shouldn't change the absolute time and it should
69        // not depend on DST, because abbreviations already contain information about DST (e.g., "PST" vs "PDT").
70        // TODO This assumption may be false for some abbreviations, see T316688#8336443.
71
72        // 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
73        // time zone abbreviations, and PHP assumes that *all* abbreviations correspond to time zones without DST.
74        // So we can't use DateTimeZone::getOffset(), and the timezone must also be specified inside the time string,
75        // and not as second argument to DateTime::__construct. See https://bugs.php.net/bug.php?id=74671
76        $randomTime = '2022-10-20 18:00:00 ' . $timezoneName;
77        $offset = ( new DateTime( $randomTime ) )->format( 'P' );
78        return new UserTimeCorrection( $offset );
79    }
80
81    /**
82     * Given a participant and an event registration, returns the timestamp when the answers of this participant should
83     * be aggregated. This method may return a timestamp in the past (e.g., if it's recent enough that we still haven't
84     * been able to aggregate the answers), but it will return null if the participant never answered any questions, or
85     * if their answers have already been aggregated. In particular, this means that if the answers have already been
86     * aggregated, the aggregation timestamp is ignored. This is motivated by the current UI, where participant whose
87     * answers have been aggregated are treated the same as those who never answered any question.
88     *
89     * @param Participant $participant
90     * @param ExistingEventRegistration $event
91     * @return string|null Timestamp in TS_UNIX format
92     */
93    public static function getAnswerAggregationTimestamp(
94        Participant $participant,
95        ExistingEventRegistration $event
96    ): ?string {
97        $firstAnswerTime = $participant->getFirstAnswerTimestamp();
98        if ( $firstAnswerTime === null || $participant->getAggregationTimestamp() !== null ) {
99            return null;
100        }
101        $participantAggregationTS = (int)$firstAnswerTime + EventAggregatedAnswersStore::ANSWERS_TTL_SEC;
102        $eventAggregationTS = (int)MWTimestamp::convert( TS_UNIX, $event->getEndUTCTimestamp() );
103        return (string)min( $participantAggregationTS, $eventAggregationTS );
104    }
105}