Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
DataOutputFormatter
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 4
552
0.00% covered (danger)
0.00%
0 / 1
 formatOutput
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 1
342
 formatNotification
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getDateHeader
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getUserLocalTime
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Notifications;
4
5use Language;
6use MediaWiki\Extension\Notifications\Formatters\EchoEventFormatter;
7use MediaWiki\Extension\Notifications\Formatters\EchoFlyoutFormatter;
8use MediaWiki\Extension\Notifications\Formatters\EchoModelFormatter;
9use MediaWiki\Extension\Notifications\Formatters\SpecialNotificationsFormatter;
10use MediaWiki\Extension\Notifications\Model\Event;
11use MediaWiki\Extension\Notifications\Model\Notification;
12use MediaWiki\Revision\RevisionRecord;
13use MediaWiki\User\User;
14use MediaWiki\Utils\MWTimestamp;
15use MediaWiki\WikiMap\WikiMap;
16use RequestContext;
17
18/**
19 * Utility class that formats a notification in the format specified
20 * @todo Make this a service with DI
21 */
22class 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}