Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.44% |
17 / 18 |
|
75.00% |
3 / 4 |
CRAP | |
0.00% |
0 / 1 |
Utils | |
94.44% |
17 / 18 |
|
75.00% |
3 / 4 |
10.02 | |
0.00% |
0 / 1 |
getWikiIDString | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
guessStringDirection | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
timezoneToUserTimeCorrection | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
getAnswerAggregationTimestamp | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | declare( strict_types=1 ); |
4 | |
5 | namespace MediaWiki\Extension\CampaignEvents; |
6 | |
7 | use DateTime; |
8 | use DateTimeZone; |
9 | use MediaWiki\DAO\WikiAwareEntity; |
10 | use MediaWiki\Extension\CampaignEvents\Event\ExistingEventRegistration; |
11 | use MediaWiki\Extension\CampaignEvents\Participants\Participant; |
12 | use MediaWiki\Extension\CampaignEvents\Questions\EventAggregatedAnswersStore; |
13 | use MediaWiki\User\UserTimeCorrection; |
14 | use MediaWiki\Utils\MWTimestamp; |
15 | use MediaWiki\WikiMap\WikiMap; |
16 | |
17 | /** |
18 | * Simple utility methods. |
19 | */ |
20 | class 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 | } |