Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 97 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
DataOutputFormatter | |
0.00% |
0 / 97 |
|
0.00% |
0 / 4 |
552 | |
0.00% |
0 / 1 |
formatOutput | |
0.00% |
0 / 86 |
|
0.00% |
0 / 1 |
342 | |||
formatNotification | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getDateHeader | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getUserLocalTime | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Notifications; |
4 | |
5 | use Language; |
6 | use MediaWiki\Extension\Notifications\Formatters\EchoEventFormatter; |
7 | use MediaWiki\Extension\Notifications\Formatters\EchoFlyoutFormatter; |
8 | use MediaWiki\Extension\Notifications\Formatters\EchoModelFormatter; |
9 | use MediaWiki\Extension\Notifications\Formatters\SpecialNotificationsFormatter; |
10 | use MediaWiki\Extension\Notifications\Model\Event; |
11 | use MediaWiki\Extension\Notifications\Model\Notification; |
12 | use MediaWiki\Revision\RevisionRecord; |
13 | use MediaWiki\User\User; |
14 | use MediaWiki\Utils\MWTimestamp; |
15 | use MediaWiki\WikiMap\WikiMap; |
16 | use RequestContext; |
17 | |
18 | /** |
19 | * Utility class that formats a notification in the format specified |
20 | * @todo Make this a service with DI |
21 | */ |
22 | class DataOutputFormatter { |
23 | |
24 | /** |
25 | * @var string[] type => class |
26 | */ |
27 | protected static $formatters = [ |
28 | 'flyout' => EchoFlyoutFormatter::class, |
29 | 'model' => EchoModelFormatter::class, |
30 | 'special' => SpecialNotificationsFormatter::class, |
31 | 'html' => SpecialNotificationsFormatter::class, |
32 | ]; |
33 | |
34 | /** |
35 | * Format a notification for a user in the format specified |
36 | * |
37 | * This method returns an array of data, some of it html |
38 | * escaped, some of it not. This confuses phan-taint-check, |
39 | * so mark it as safe for html and safe to be escaped again. |
40 | * @return-taint onlysafefor_htmlnoent |
41 | * |
42 | * @param Notification $notification |
43 | * @param string|false $format Output format, false to not format any notifications |
44 | * @param User $user the target user viewing the notification |
45 | * @param Language $lang Language to format the notification in |
46 | * @return array|false False if it could not be formatted |
47 | */ |
48 | public static function formatOutput( |
49 | Notification $notification, |
50 | $format, |
51 | User $user, |
52 | Language $lang |
53 | ) { |
54 | $event = $notification->getEvent(); |
55 | $timestamp = $notification->getTimestamp(); |
56 | $utcTimestampIso8601 = wfTimestamp( TS_ISO_8601, $timestamp ); |
57 | $utcTimestampUnix = (int)wfTimestamp( TS_UNIX, $timestamp ); |
58 | $utcTimestampMW = wfTimestamp( TS_MW, $timestamp ); |
59 | $bundledIds = null; |
60 | |
61 | $bundledNotifs = $notification->getBundledNotifications(); |
62 | if ( $bundledNotifs ) { |
63 | $bundledEvents = array_map( static function ( Notification $notif ) { |
64 | return $notif->getEvent(); |
65 | }, $bundledNotifs ); |
66 | $event->setBundledEvents( $bundledEvents ); |
67 | |
68 | $bundledIds = array_map( static function ( $event ) { |
69 | return (int)$event->getId(); |
70 | }, $bundledEvents ); |
71 | } |
72 | |
73 | $timestampMw = self::getUserLocalTime( $user, $timestamp ); |
74 | |
75 | // Start creating date section header |
76 | $now = (int)wfTimestamp(); |
77 | $dateFormat = substr( $timestampMw, 0, 8 ); |
78 | $timeDiff = $now - $utcTimestampUnix; |
79 | // Most notifications would be more than two days ago, check this |
80 | // first instead of checking 'today' then 'yesterday' |
81 | if ( $timeDiff > 172800 ) { |
82 | $date = self::getDateHeader( $user, $timestampMw ); |
83 | // 'Today' |
84 | } elseif ( str_starts_with( self::getUserLocalTime( $user, $now ), $dateFormat ) ) { |
85 | $date = wfMessage( 'echo-date-today' )->escaped(); |
86 | // 'Yesterday' |
87 | } elseif ( str_starts_with( self::getUserLocalTime( $user, $now - 86400 ), $dateFormat ) ) { |
88 | $date = wfMessage( 'echo-date-yesterday' )->escaped(); |
89 | } else { |
90 | $date = self::getDateHeader( $user, $timestampMw ); |
91 | } |
92 | // End creating date section header |
93 | |
94 | $output = [ |
95 | 'wiki' => WikiMap::getCurrentWikiId(), |
96 | 'id' => $event->getId(), |
97 | 'type' => $event->getType(), |
98 | 'category' => $event->getCategory(), |
99 | 'section' => $event->getSection(), |
100 | 'timestamp' => [ |
101 | // ISO 8601 is supposed to be the *only* format used for |
102 | // date output, but back-compat... |
103 | 'utciso8601' => $utcTimestampIso8601, |
104 | |
105 | // UTC timestamp in UNIX format used for loading more notification |
106 | 'utcunix' => $utcTimestampUnix, |
107 | 'unix' => self::getUserLocalTime( $user, $timestamp, TS_UNIX ), |
108 | 'utcmw' => $utcTimestampMW, |
109 | 'mw' => $timestampMw, |
110 | 'date' => $date |
111 | ], |
112 | ]; |
113 | |
114 | if ( $bundledIds ) { |
115 | $output['bundledIds'] = $bundledIds; |
116 | } |
117 | |
118 | if ( $event->getVariant() ) { |
119 | $output['variant'] = $event->getVariant(); |
120 | } |
121 | |
122 | $title = $event->getTitle(); |
123 | if ( $title ) { |
124 | $output['title'] = [ |
125 | 'full' => $title->getPrefixedText(), |
126 | 'namespace' => $title->getNsText(), |
127 | 'namespace-key' => $title->getNamespace(), |
128 | 'text' => $title->getText(), |
129 | ]; |
130 | } |
131 | |
132 | $agent = $event->getAgent(); |
133 | if ( $agent ) { |
134 | if ( $event->userCan( RevisionRecord::DELETED_USER, $user ) ) { |
135 | $output['agent'] = [ |
136 | 'id' => $agent->getId(), |
137 | 'name' => $agent->getName(), |
138 | ]; |
139 | } else { |
140 | $output['agent'] = [ 'userhidden' => '' ]; |
141 | } |
142 | } |
143 | |
144 | if ( $event->getRevision() ) { |
145 | $output['revid'] = $event->getRevision()->getId(); |
146 | } |
147 | |
148 | if ( $notification->getReadTimestamp() ) { |
149 | $output['read'] = $notification->getReadTimestamp(); |
150 | } |
151 | |
152 | // This is only meant for unread notifications, if a notification has a target |
153 | // page, then it shouldn't be auto marked as read unless the user visits |
154 | // the target page or a user marks it as read manually ( coming soon ) |
155 | $output['targetpages'] = []; |
156 | if ( $notification->getTargetPages() ) { |
157 | foreach ( $notification->getTargetPages() as $targetPage ) { |
158 | $output['targetpages'][] = $targetPage->getPageId(); |
159 | } |
160 | } |
161 | |
162 | if ( $format ) { |
163 | $formatted = self::formatNotification( $event, $user, $format, $lang ); |
164 | if ( $formatted === false ) { |
165 | // Can't display it, so mark it as read |
166 | DeferredMarkAsDeletedUpdate::add( $event ); |
167 | return false; |
168 | } |
169 | $output['*'] = $formatted; |
170 | |
171 | if ( $notification->getBundledNotifications() && |
172 | Services::getInstance()->getAttributeManager()->isBundleExpandable( $event->getType() ) |
173 | ) { |
174 | $output['bundledNotifications'] = array_values( array_filter( array_map( |
175 | static function ( Notification $notification ) use ( $format, $user, $lang ) { |
176 | // remove nested notifications to |
177 | // - ensure they are formatted as single notifications (not bundled) |
178 | // - prevent further re-entrance on the current notification |
179 | $notification->setBundledNotifications( [] ); |
180 | $notification->getEvent()->setBundledEvents( [] ); |
181 | return self::formatOutput( $notification, $format, $user, $lang ); |
182 | }, |
183 | array_merge( [ $notification ], $notification->getBundledNotifications() ) |
184 | ) ) ); |
185 | } |
186 | } |
187 | |
188 | return $output; |
189 | } |
190 | |
191 | /** |
192 | * @param Event $event |
193 | * @param User $user |
194 | * @param string $format |
195 | * @param Language $lang |
196 | * @return string[]|string|false False if it could not be formatted |
197 | */ |
198 | protected static function formatNotification( Event $event, User $user, $format, $lang ) { |
199 | if ( isset( self::$formatters[$format] ) ) { |
200 | $class = self::$formatters[$format]; |
201 | /** @var EchoEventFormatter $formatter */ |
202 | $formatter = new $class( $user, $lang ); |
203 | return $formatter->format( $event ); |
204 | } |
205 | |
206 | return false; |
207 | } |
208 | |
209 | /** |
210 | * Get the date header in user's format, 'May 10' or '10 May', depending |
211 | * on user's date format preference |
212 | * @param User $user |
213 | * @param string $timestampMw |
214 | * @return string |
215 | */ |
216 | protected static function getDateHeader( User $user, $timestampMw ) { |
217 | $lang = RequestContext::getMain()->getLanguage(); |
218 | $dateFormat = $lang->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' ); |
219 | |
220 | return $lang->sprintfDate( $dateFormat, $timestampMw ); |
221 | } |
222 | |
223 | /** |
224 | * Helper function for converting UTC timezone to a user's timezone |
225 | * |
226 | * @param User $user |
227 | * @param string|int $ts |
228 | * @param int $format output format |
229 | * |
230 | * @return string |
231 | */ |
232 | public static function getUserLocalTime( User $user, $ts, $format = TS_MW ) { |
233 | $timestamp = new MWTimestamp( $ts ); |
234 | $timestamp->offsetForUser( $user ); |
235 | |
236 | return $timestamp->getTimestamp( $format ); |
237 | } |
238 | |
239 | } |