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 | $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 | } |