Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 98 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
| ChangesListFormatter | |
0.00% |
0 / 98 |
|
0.00% |
0 / 8 |
600 | |
0.00% |
0 / 1 |
| getHistoryType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| format | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
20 | |||
| getEditSummary | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
| getTitleLink | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
| getTimestampLink | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
| getLogTextLinks | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
30 | |||
| getFlags | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
| formatFlags | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace Flow\Formatter; |
| 4 | |
| 5 | use Flow\Conversion\Utils; |
| 6 | use Flow\Data\Listener\RecentChangesListener; |
| 7 | use Flow\Exception\FlowException; |
| 8 | use Flow\Exception\PermissionException; |
| 9 | use Flow\Model\Anchor; |
| 10 | use Flow\Model\UUID; |
| 11 | use MediaWiki\Context\IContextSource; |
| 12 | use MediaWiki\MediaWikiServices; |
| 13 | use MediaWiki\RecentChanges\ChangesList; |
| 14 | use MediaWiki\RecentChanges\RCCacheEntry; |
| 15 | |
| 16 | class ChangesListFormatter extends AbstractFormatter { |
| 17 | protected function getHistoryType() { |
| 18 | return 'recentchanges'; |
| 19 | } |
| 20 | |
| 21 | /** |
| 22 | * @param RecentChangesRow $row |
| 23 | * @param IContextSource $ctx |
| 24 | * @param bool $linkOnly |
| 25 | * @return string|false Output line, or false on failure |
| 26 | * @throws FlowException |
| 27 | */ |
| 28 | public function format( RecentChangesRow $row, IContextSource $ctx, $linkOnly = false ) { |
| 29 | $this->serializer->setIncludeHistoryProperties( true ); |
| 30 | $this->serializer->setIncludeContent( false ); |
| 31 | |
| 32 | $data = $this->serializer->formatApi( $row, $ctx, 'recentchanges' ); |
| 33 | if ( !$data ) { |
| 34 | return false; |
| 35 | } |
| 36 | |
| 37 | if ( $linkOnly ) { |
| 38 | return $this->getTitleLink( $data, $row, $ctx ); |
| 39 | } |
| 40 | |
| 41 | // The ' . . ' text between elements |
| 42 | $separator = $this->changeSeparator(); |
| 43 | |
| 44 | $links = []; |
| 45 | $links[] = $this->getDiffAnchor( $data['links'], $ctx ); |
| 46 | $links[] = $this->getHistAnchor( $data['links'], $ctx ); |
| 47 | |
| 48 | $description = $this->formatDescription( $data, $ctx ); |
| 49 | |
| 50 | $flags = $this->getFlags( $row, $ctx ); |
| 51 | |
| 52 | return $this->formatAnchorsAsPipeList( $links, $ctx ) . |
| 53 | $separator . |
| 54 | $this->formatFlags( $flags ) . |
| 55 | $this->getTitleLink( $data, $row, $ctx ) . |
| 56 | $ctx->msg( 'semicolon-separator' )->escaped() . |
| 57 | ' ' . |
| 58 | $this->formatTimestamp( $data, 'time' ) . |
| 59 | $separator . |
| 60 | ChangesList::showCharacterDifference( |
| 61 | $data['size']['old'], |
| 62 | $data['size']['new'], |
| 63 | $ctx |
| 64 | ) . |
| 65 | ( Utils::htmlToPlaintext( $description ) ? $separator . $description : '' ) . |
| 66 | $this->getEditSummary( $row, $ctx, $data ); |
| 67 | } |
| 68 | |
| 69 | /** |
| 70 | * @param RecentChangesRow $row |
| 71 | * @param IContextSource $ctx |
| 72 | * @param array $data |
| 73 | * @return string |
| 74 | */ |
| 75 | public function getEditSummary( RecentChangesRow $row, IContextSource $ctx, array $data ) { |
| 76 | // Build description message, piggybacking on history i18n |
| 77 | $changeType = $data['changeType']; |
| 78 | $actions = $this->permissions->getActions(); |
| 79 | |
| 80 | $key = $actions->getValue( $changeType, 'history', 'i18n-message' ); |
| 81 | // Find specialized message for summary |
| 82 | // i18n messages: flow-rev-message-new-post-recentchanges-summary, |
| 83 | // flow-rev-message-edit-post-recentchanges-summary |
| 84 | $msg = $ctx->msg( $key . '-' . $this->getHistoryType() . '-summary' ); |
| 85 | if ( !$msg->exists() ) { |
| 86 | // No summary for this action |
| 87 | return ''; |
| 88 | } |
| 89 | |
| 90 | $msg = $msg->params( $this->getDescriptionParams( $data, $actions, $changeType ) ); |
| 91 | |
| 92 | // Below code is inspired by MediaWiki\CommentFormatter\CommentParser::doSectionLinks |
| 93 | $prefix = $ctx->msg( 'autocomment-prefix' )->inContentLanguage()->escaped(); |
| 94 | $link = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( |
| 95 | $row->workflow->getOwnerTitle(), |
| 96 | $ctx->getLanguage()->getArrow( 'backwards' ) |
| 97 | ); |
| 98 | $summary = '<span class="autocomment">' . $msg->text() . '</span>'; |
| 99 | |
| 100 | // '(' + '' + '←' + summary + ')' |
| 101 | $text = MediaWikiServices::getInstance()->getCommentFormatter() |
| 102 | ->formatBlock( $prefix . $link . $summary ); |
| 103 | |
| 104 | // MediaWiki\CommentFormatter\CommentFormatter::formatBlock escaped everything, but what we built was safe |
| 105 | // and should not be escaped so let's go back to decoded entities |
| 106 | return htmlspecialchars_decode( $text ); |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * This overrides the default title link to include highlights for the posts |
| 111 | * that have not yet been seen. |
| 112 | * |
| 113 | * @param array $data |
| 114 | * @param FormatterRow $row |
| 115 | * @param IContextSource $ctx |
| 116 | * @return string |
| 117 | */ |
| 118 | protected function getTitleLink( array $data, FormatterRow $row, IContextSource $ctx ) { |
| 119 | if ( !$row instanceof RecentChangesRow ) { |
| 120 | // actually, this should be typehint, but can't because this needs |
| 121 | // to match the parent's more generic typehint |
| 122 | return parent::getTitleLink( $data, $row, $ctx ); |
| 123 | } |
| 124 | |
| 125 | if ( !isset( $data['links']['topic'] ) || !$data['links']['topic'] instanceof Anchor ) { |
| 126 | // no valid title anchor (probably header entry) |
| 127 | return parent::getTitleLink( $data, $row, $ctx ); |
| 128 | } |
| 129 | |
| 130 | $watched = $row->recentChange->getAttribute( 'wl_notificationtimestamp' ); |
| 131 | if ( is_bool( $watched ) ) { |
| 132 | // RC & watchlist share most code; the latter is unaware of when |
| 133 | // something was watched though, so we'll ignore that here |
| 134 | return parent::getTitleLink( $data, $row, $ctx ); |
| 135 | } |
| 136 | |
| 137 | if ( $watched === null ) { |
| 138 | // there is no data for unread posts - they've all been seen |
| 139 | return parent::getTitleLink( $data, $row, $ctx ); |
| 140 | } |
| 141 | |
| 142 | // get comparison UUID corresponding to this last watched timestamp |
| 143 | $uuid = UUID::getComparisonUUID( $watched ); |
| 144 | |
| 145 | // add highlight details to anchor |
| 146 | /** @var Anchor $anchor */ |
| 147 | $anchor = clone $data['links']['topic']; |
| 148 | $anchor->query['fromnotif'] = '1'; |
| 149 | $anchor->fragment = '#flow-post-' . $uuid->getAlphadecimal(); |
| 150 | $data['links']['topic'] = $anchor; |
| 151 | |
| 152 | // now pass it on to parent with the new, updated, link ;) |
| 153 | return parent::getTitleLink( $data, $row, $ctx ); |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * @param RecentChangesRow $row |
| 158 | * @param IContextSource $ctx |
| 159 | * @return string |
| 160 | * @throws PermissionException |
| 161 | */ |
| 162 | public function getTimestampLink( $row, $ctx ) { |
| 163 | $this->serializer->setIncludeHistoryProperties( true ); |
| 164 | $this->serializer->setIncludeContent( false ); |
| 165 | |
| 166 | $data = $this->serializer->formatApi( $row, $ctx, 'recentchanges' ); |
| 167 | if ( $data === false ) { |
| 168 | throw new PermissionException( 'Insufficient permissions for ' . $row->revision->getRevisionId()->getAlphadecimal() ); |
| 169 | } |
| 170 | |
| 171 | return $this->formatTimestamp( $data, 'time' ); |
| 172 | } |
| 173 | |
| 174 | /** |
| 175 | * @param RecentChangesRow $row |
| 176 | * @param IContextSource $ctx |
| 177 | * @param RCCacheEntry[] $block |
| 178 | * @param array $links |
| 179 | * @return array|false Links array, or false on failure |
| 180 | * @throws FlowException |
| 181 | * @throws \Flow\Exception\InvalidInputException |
| 182 | */ |
| 183 | public function getLogTextLinks( RecentChangesRow $row, IContextSource $ctx, array $block, array $links = [] ) { |
| 184 | $data = $this->serializer->formatApi( $row, $ctx, 'recentchanges' ); |
| 185 | if ( !$data ) { |
| 186 | return false; |
| 187 | } |
| 188 | |
| 189 | // Find the last (oldest) row in $block that is a Flow row. Note that there can be non-Flow |
| 190 | // things in $block (T228290). |
| 191 | $flowRows = array_filter( $block, static function ( $blockRow ) { |
| 192 | return $blockRow->getAttribute( 'rc_source' ) === RecentChangesListener::SRC_FLOW; |
| 193 | } ); |
| 194 | $oldestRow = end( $flowRows ) ?? $row->recentChange; |
| 195 | |
| 196 | $old = unserialize( $oldestRow->getAttribute( 'rc_params' ) ); |
| 197 | $oldId = $old ? UUID::create( $old['flow-workflow-change']['revision'] ) : $row->revision->getRevisionId(); |
| 198 | |
| 199 | if ( isset( $data['links']['topic'] ) ) { |
| 200 | // add highlight details to anchor |
| 201 | // FIXME: This doesn't work well if the different rows in $block are for different topics |
| 202 | /** @var Anchor $anchor */ |
| 203 | $anchor = clone $data['links']['topic']; |
| 204 | $anchor->query['fromnotif'] = '1'; |
| 205 | $anchor->fragment = '#flow-post-' . $oldId->getAlphadecimal(); |
| 206 | } elseif ( isset( $data['links']['workflow'] ) ) { |
| 207 | $anchor = $data['links']['workflow']; |
| 208 | } else { |
| 209 | // this will be caught and logged by the RC hook, it will not fatal the page. |
| 210 | throw new FlowException( "No anchor available for revision $oldId" ); |
| 211 | } |
| 212 | |
| 213 | $changes = count( $block ); |
| 214 | // link text: "n changes" |
| 215 | $text = $ctx->msg( 'nchanges' )->numParams( $changes )->escaped(); |
| 216 | |
| 217 | // override total changes link |
| 218 | $links['total-changes'] = $anchor->toHtml( $text ); |
| 219 | |
| 220 | return $links; |
| 221 | } |
| 222 | |
| 223 | /** |
| 224 | * @param RecentChangesRow $row |
| 225 | * @param IContextSource $ctx |
| 226 | * @return array |
| 227 | */ |
| 228 | public function getFlags( RecentChangesRow $row, IContextSource $ctx ) { |
| 229 | return [ |
| 230 | 'newpage' => $row->isFirstReply && $row->revision->isFirstRevision(), |
| 231 | 'minor' => false, |
| 232 | 'unpatrolled' => ChangesList::isUnpatrolled( $row->recentChange, $ctx->getUser() ), |
| 233 | 'bot' => false, |
| 234 | ]; |
| 235 | } |
| 236 | |
| 237 | /** |
| 238 | * @param array $flags |
| 239 | * @return string |
| 240 | */ |
| 241 | protected function formatFlags( $flags ) { |
| 242 | $flagKeys = array_keys( array_filter( $flags ) ); |
| 243 | if ( $flagKeys ) { |
| 244 | $formattedFlags = array_map( [ ChangesList::class, 'flag' ], $flagKeys ); |
| 245 | return implode( ' ', $formattedFlags ) . ' '; |
| 246 | } |
| 247 | return ''; |
| 248 | } |
| 249 | } |