Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MobileSpecialPageFeed
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 6
1122
0.00% covered (danger)
0.00%
0 / 1
 execute
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 formatComment
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 renderListHeaderWhereNeeded
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 getRevisionCommentHTML
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 getUsernameText
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 renderFeedItemHtml
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
240
1<?php
2
3use MediaWiki\Html\Html;
4use MediaWiki\Parser\Sanitizer;
5use MediaWiki\Permissions\Authority;
6use MediaWiki\Revision\RevisionRecord;
7use MediaWiki\Title\Title;
8use Wikimedia\IPUtils;
9
10/**
11 * This is an abstract class intended for use by special pages that consist primarily of
12 * a list of pages, for example, Special:Watchlist or Special:History.
13 */
14abstract class MobileSpecialPageFeed extends MobileSpecialPage {
15    /** @var bool Whether to show the username in results or not */
16    protected $showUsername = true;
17    /** @var string */
18    protected $lastDate;
19    /** @var Title|null */
20    protected $title;
21
22    /**
23     * Render the special page content
24     * @param string|null $par parameters submitted as subpage
25     */
26    public function execute( $par ) {
27        $out = $this->getOutput();
28        $out->addModuleStyles( [
29            // FIXME: This module should be removed when the following tickets are resolved:
30            // * T305113
31            // * T109277
32            // * T117279
33            'mobile.special.pagefeed.styles',
34        ] );
35        $this->setHeaders();
36        parent::execute( $par );
37    }
38
39    /**
40     * Formats an edit comment
41     * @param string $comment The raw comment text
42     * @param Title $title The title of the page that was edited
43     *
44     * @return string HTML code
45     */
46    protected function formatComment( $comment, $title ) {
47        if ( $comment === '' ) {
48            $comment = $this->msg( 'mobile-frontend-changeslist-nocomment' )->plain();
49        } else {
50            $comment = $this->commentFormatter->format( $comment, $title );
51            // flatten back to text
52            $comment = htmlspecialchars( Sanitizer::stripAllTags( $comment ) );
53        }
54        return $comment;
55    }
56
57    /**
58     * Renders a date header when necessary.
59     * FIXME: Juliusz won't like this function.
60     * @param string $date The date of the current item
61     */
62    protected function renderListHeaderWhereNeeded( $date ) {
63        if ( !isset( $this->lastDate ) || $date !== $this->lastDate ) {
64            $output = $this->getOutput();
65            if ( isset( $this->lastDate ) ) {
66                $output->addHTML(
67                    Html::closeElement( 'ul' )
68                );
69            }
70            $output->addHTML(
71                Html::element( 'h2', [ 'class' => 'list-header' ], $date ) .
72                Html::openElement( 'ul', [
73                    // TODO remove page-list after initial release T337741
74                        'class' => 'mw-mf-page-list diff-summary-list side-list'
75                    ]
76                )
77            );
78        }
79        $this->lastDate = $date;
80    }
81
82    /**
83     * Generates revision text based on user's rights and preference
84     * @param RevisionRecord $rev
85     * @param Authority $user viewing the revision
86     * @param bool $unhide whether the user wants to see hidden comments
87     *   if the user doesn't have permission, comment will display as rev-deleted-comment
88     * @return string plain text label
89     */
90    protected function getRevisionCommentHTML( RevisionRecord $rev, $user, $unhide ) {
91        if ( RevisionRecord::userCanBitfield(
92            $rev->getVisibility(),
93            RevisionRecord::DELETED_COMMENT,
94            $user
95        ) ) {
96            if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) && !$unhide ) {
97                $comment = $this->msg( 'rev-deleted-comment' )->escaped();
98            } else {
99                $commentObj = $rev->getComment( RevisionRecord::FOR_THIS_USER, $user );
100                $commentText = $commentObj ? $commentObj->text : '';
101
102                // escape any HTML in summary and add CSS for any auto-generated comments
103                $comment = $this->formatComment( $commentText, $this->title );
104            }
105        } else {
106            // Confusingly "Revision::userCan" Determines if the current user is
107            // allowed to view a particular field of this revision, /if/ it's marked as
108            // deleted. This will only get executed in event a comment has been deleted
109            // and user cannot view it.
110            $comment = $this->msg( 'rev-deleted-comment' )->escaped();
111        }
112        return $comment;
113    }
114
115    /**
116     * Generates username text based on user's rights and preference
117     * @param RevisionRecord $rev
118     * @param Authority $user viewing the revision
119     * @param bool $unhide whether the user wants to see hidden usernames
120     * @return string plain text label
121     */
122    protected function getUsernameText( $rev, $user, $unhide ) {
123        $revUser = $rev->getUser( RevisionRecord::FOR_THIS_USER, $user );
124        if ( $revUser && $revUser->isRegistered() ) {
125            $username = $revUser->getName();
126        } else {
127            $revUser = $rev->getUser( RevisionRecord::RAW );
128            $username = IPUtils::prettifyIP( $revUser->getName() ) ?? $revUser->getName();
129        }
130        if (
131            !RevisionRecord::userCanBitfield(
132                $rev->getVisibility(),
133                RevisionRecord::DELETED_USER,
134                $user
135            ) ||
136            ( $rev->isDeleted( RevisionRecord::DELETED_USER ) && !$unhide )
137        ) {
138            $username = $this->msg( 'rev-deleted-user' )->text();
139        }
140        return $username;
141    }
142
143    /**
144     * Renders an item in the feed
145     *
146     * @param array $options An array of various options for
147     *    rendering the feed item's HTML e.g.
148     *
149     *    [
150     *        'ts'       => MWTimestamp - The time the edit occurred
151     *        'diffLink' => string      - The URL to the diff for the edit
152     *        'username' => string      - The username of the user that made
153     *                                    the edit (absent if anonymous)
154     *        'comment'  => string      - The edit summary, HTML escaped
155     *        'title'    => Title|null  - The title of the page that was edited
156     *        'isAnon'   => bool        - Is the edit anonymous?
157     *        'bytes'    => int|null    - Net number of bytes changed or null
158     *                                    if not applicable
159     *        'isMinor'  => bool        - Is the edit minor?
160     *   ];
161     *
162     */
163    protected function renderFeedItemHtml( array $options ): void {
164        $output = $this->getOutput();
165        $user = $this->getUser();
166        $lang = $this->getLanguage();
167
168        if ( (bool)( $options['isAnon'] ?? false ) ) {
169            $usernameClass = 'mw-mf-user mw-mf-anon';
170            $iconHTML = MobileUI::icon( 'userAnonymous' );
171        } else {
172            $usernameClass = 'mw-mf-user';
173            $iconHTML = MobileUI::icon( 'userAvatar' );
174        }
175
176        // Add whitespace between icon and label.
177        $iconHTML .= ' ';
178
179        $html = Html::openElement( 'li', [ 'class' => 'page-summary' ] );
180
181        if ( isset( $options['diffLink'] ) && $options['diffLink'] ) {
182            $html .= Html::openElement( 'a', [ 'href' => $options['diffLink'], 'class' => 'title' ] );
183        } else {
184            $html .= Html::openElement( 'div', [ 'class' => 'title' ] );
185        }
186
187        if ( isset( $options['title'] ) && $options['title'] ) {
188            $html .= Html::element( 'h3', [], $options['title']->getPrefixedText() );
189        }
190
191        if ( isset( $options['username'] ) && $options['username'] && $this->showUsername ) {
192            $html .= Html::rawElement( 'p', [ 'class' => $usernameClass ],
193                $iconHTML . ' ' .
194                Html::element( 'span', [], $options['username'] )
195            );
196        }
197
198        $html .= Html::rawElement(
199            'p',
200            [ 'class' => 'edit-summary component truncated-text multi-line two-line' ],
201            $options['comment'] ?? ''
202        );
203
204        if ( (bool)( $options['isMinor'] ?? false ) ) {
205            $html .= ChangesList::flag( 'minor' );
206        }
207
208        $html .= Html::openElement( 'div', [ 'class' => 'list-thumb' ] ) .
209            Html::element( 'p', [ 'class' => 'timestamp' ], $lang->userTime( $options['ts'], $user ) );
210
211        if ( isset( $options['bytes'] ) && $options['bytes'] ) {
212            $bytes = $options['bytes'];
213            $formattedBytes = $lang->formatNum( $bytes );
214            if ( $bytes > 0 ) {
215                $formattedBytes = '+' . $formattedBytes;
216                $bytesClass = 'mw-mf-bytesadded';
217            } else {
218                $bytesClass = 'mw-mf-bytesremoved';
219            }
220            $html .= Html::element(
221                'p',
222                [
223                    'class' => $bytesClass,
224                    'dir' => 'ltr',
225                ],
226                $formattedBytes
227            );
228        }
229
230        $html .= Html::closeElement( 'div' );
231
232        if ( isset( $options['diffLink'] ) && $options['diffLink'] ) {
233            $html .= Html::closeElement( 'a' );
234        } else {
235            $html .= Html::closeElement( 'div' );
236        }
237        $html .= Html::closeElement( 'li' );
238
239        $output->addHTML( $html );
240    }
241}