Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
14.93% |
20 / 134 |
|
16.67% |
1 / 6 |
CRAP | |
0.00% |
0 / 1 |
FeedUtils | |
15.04% |
20 / 133 |
|
16.67% |
1 / 6 |
544.79 | |
0.00% |
0 / 1 |
checkFeedOutput | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
formatDiff | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
formatDiffRow | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
formatDiffRow2 | |
0.00% |
0 / 72 |
|
0.00% |
0 / 1 |
272 | |||
getDiffLink | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
applyDiffStyle | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Helper functions for feeds. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup Feed |
22 | */ |
23 | |
24 | namespace MediaWiki\Feed; |
25 | |
26 | use MediaWiki\Content\TextContent; |
27 | use MediaWiki\Context\DerivativeContext; |
28 | use MediaWiki\Context\RequestContext; |
29 | use MediaWiki\Html\Html; |
30 | use MediaWiki\MainConfigNames; |
31 | use MediaWiki\MediaWikiServices; |
32 | use MediaWiki\Output\OutputPage; |
33 | use MediaWiki\Revision\RevisionRecord; |
34 | use MediaWiki\Revision\SlotRecord; |
35 | use MediaWiki\Title\Title; |
36 | use UtfNormal; |
37 | |
38 | /** |
39 | * Helper functions for feeds |
40 | * |
41 | * @ingroup Feed |
42 | */ |
43 | class FeedUtils { |
44 | |
45 | /** |
46 | * Check whether feeds can be used and that $type is a valid feed type |
47 | * |
48 | * @param string $type Feed type, as requested by the user |
49 | * @param OutputPage|null $output Null falls back to $wgOut |
50 | * @return bool |
51 | * @since 1.36 $output parameter added |
52 | * |
53 | */ |
54 | public static function checkFeedOutput( $type, $output = null ) { |
55 | $feed = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::Feed ); |
56 | $feedClasses = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FeedClasses ); |
57 | if ( $output === null ) { |
58 | // Todo update GoogleNewsSitemap and deprecate |
59 | global $wgOut; |
60 | $output = $wgOut; |
61 | } |
62 | |
63 | if ( !$feed ) { |
64 | $output->addWikiMsg( 'feed-unavailable' ); |
65 | return false; |
66 | } |
67 | |
68 | if ( !isset( $feedClasses[$type] ) ) { |
69 | $output->addWikiMsg( 'feed-invalid' ); |
70 | return false; |
71 | } |
72 | |
73 | return true; |
74 | } |
75 | |
76 | /** |
77 | * Format a diff for the newsfeed |
78 | * |
79 | * @param \stdClass $row Row from the recentchanges table, including fields as |
80 | * appropriate for CommentStore |
81 | * @param string|null $formattedComment rc_comment in HTML format, or null |
82 | * to format it on demand. |
83 | * @return string |
84 | */ |
85 | public static function formatDiff( $row, $formattedComment = null ) { |
86 | $titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title ); |
87 | $timestamp = wfTimestamp( TS_MW, $row->rc_timestamp ); |
88 | $actiontext = ''; |
89 | if ( $row->rc_type == RC_LOG ) { |
90 | $rcRow = (array)$row; // newFromRow() only accepts arrays for RC rows |
91 | $actiontext = MediaWikiServices::getInstance()->getLogFormatterFactory() |
92 | ->newFromRow( $rcRow )->getActionText(); |
93 | } |
94 | if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) { |
95 | $formattedComment = wfMessage( 'rev-deleted-comment' )->escaped(); |
96 | } elseif ( $formattedComment === null ) { |
97 | $services = MediaWikiServices::getInstance(); |
98 | $formattedComment = $services->getCommentFormatter()->format( |
99 | $services->getCommentStore()->getComment( 'rc_comment', $row )->text ); |
100 | } |
101 | return self::formatDiffRow2( $titleObj, |
102 | $row->rc_last_oldid, $row->rc_this_oldid, |
103 | $timestamp, |
104 | $formattedComment, |
105 | $actiontext |
106 | ); |
107 | } |
108 | |
109 | /** |
110 | * Really format a diff for the newsfeed |
111 | * |
112 | * @param Title $title |
113 | * @param int $oldid Old revision's id |
114 | * @param int $newid New revision's id |
115 | * @param string $timestamp New revision's timestamp |
116 | * @param string $comment New revision's comment |
117 | * @param string $actiontext Text of the action; in case of log event |
118 | * @return string |
119 | * @deprecated since 1.38 use formatDiffRow2 |
120 | * |
121 | */ |
122 | public static function formatDiffRow( $title, $oldid, $newid, $timestamp, |
123 | $comment, $actiontext = '' |
124 | ) { |
125 | $formattedComment = MediaWikiServices::getInstance()->getCommentFormatter() |
126 | ->format( $comment ); |
127 | return self::formatDiffRow2( $title, $oldid, $newid, $timestamp, |
128 | $formattedComment, $actiontext ); |
129 | } |
130 | |
131 | /** |
132 | * Really really format a diff for the newsfeed. Same as formatDiffRow() |
133 | * except with preformatted comments. |
134 | * |
135 | * @param Title $title |
136 | * @param int $oldid Old revision's id |
137 | * @param int $newid New revision's id |
138 | * @param string $timestamp New revision's timestamp |
139 | * @param string $formattedComment New revision's comment in HTML format |
140 | * @param string $actiontext Text of the action; in case of log event |
141 | * @return string |
142 | */ |
143 | public static function formatDiffRow2( |
144 | $title, $oldid, $newid, $timestamp, $formattedComment, $actiontext = '' |
145 | ) { |
146 | $feedDiffCutoff = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FeedDiffCutoff ); |
147 | |
148 | // log entries |
149 | $unwrappedText = implode( |
150 | ' ', |
151 | array_filter( [ $actiontext, $formattedComment ] ) |
152 | ); |
153 | $completeText = Html::rawElement( 'p', [], $unwrappedText ) . "\n"; |
154 | |
155 | // NOTE: Check permissions for anonymous users, not current user. |
156 | // No "privileged" version should end up in the cache. |
157 | // Most feed readers will not log in anyway. |
158 | $services = MediaWikiServices::getInstance(); |
159 | $anon = $services->getUserFactory()->newAnonymous(); |
160 | $permManager = $services->getPermissionManager(); |
161 | $userCan = $permManager->userCan( |
162 | 'read', |
163 | $anon, |
164 | $title |
165 | ); |
166 | |
167 | // Can't diff special pages, unreadable pages or pages with no new revision |
168 | // to compare against: just return the text. |
169 | if ( $title->getNamespace() < 0 || !$userCan || !$newid ) { |
170 | return $completeText; |
171 | } |
172 | |
173 | $revLookup = $services->getRevisionLookup(); |
174 | $contentHandlerFactory = $services->getContentHandlerFactory(); |
175 | if ( $oldid ) { |
176 | $diffText = ''; |
177 | // Don't bother generating the diff if we won't be able to show it |
178 | if ( $feedDiffCutoff > 0 ) { |
179 | $revRecord = $revLookup->getRevisionById( $oldid ); |
180 | |
181 | if ( !$revRecord ) { |
182 | $diffText = false; |
183 | } else { |
184 | $context = new DerivativeContext( RequestContext::getMain() ); |
185 | $context->setTitle( $title ); |
186 | |
187 | $model = $revRecord->getSlot( |
188 | SlotRecord::MAIN, |
189 | RevisionRecord::RAW |
190 | )->getModel(); |
191 | $contentHandler = $contentHandlerFactory->getContentHandler( $model ); |
192 | $de = $contentHandler->createDifferenceEngine( $context, $oldid, $newid ); |
193 | $lang = $context->getLanguage(); |
194 | $user = $context->getUser(); |
195 | $diffText = $de->getDiff( |
196 | $context->msg( 'previousrevision' )->text(), // hack |
197 | $context->msg( 'revisionasof', |
198 | $lang->userTimeAndDate( $timestamp, $user ), |
199 | $lang->userDate( $timestamp, $user ), |
200 | $lang->userTime( $timestamp, $user ) )->text() ); |
201 | } |
202 | } |
203 | |
204 | if ( $feedDiffCutoff <= 0 || ( strlen( $diffText ) > $feedDiffCutoff ) ) { |
205 | // Omit large diffs |
206 | $diffText = self::getDiffLink( $title, $newid, $oldid ); |
207 | } elseif ( $diffText === false ) { |
208 | // Error in diff engine, probably a missing revision |
209 | $diffText = Html::rawElement( |
210 | 'p', |
211 | [], |
212 | "Can't load revision $newid" |
213 | ); |
214 | } else { |
215 | // Diff output fine, clean up any illegal UTF-8 |
216 | $diffText = UtfNormal\Validator::cleanUp( $diffText ); |
217 | $diffText = self::applyDiffStyle( $diffText ); |
218 | } |
219 | } else { |
220 | $revRecord = $revLookup->getRevisionById( $newid ); |
221 | if ( $feedDiffCutoff <= 0 || $revRecord === null ) { |
222 | $newContent = $contentHandlerFactory |
223 | ->getContentHandler( $title->getContentModel() ) |
224 | ->makeEmptyContent(); |
225 | } else { |
226 | $newContent = $revRecord->getContent( SlotRecord::MAIN ); |
227 | } |
228 | |
229 | if ( $newContent instanceof TextContent ) { |
230 | // only textual content has a "source view". |
231 | $text = $newContent->getText(); |
232 | |
233 | if ( $feedDiffCutoff <= 0 || strlen( $text ) > $feedDiffCutoff ) { |
234 | $html = null; |
235 | } else { |
236 | $html = nl2br( htmlspecialchars( $text ) ); |
237 | } |
238 | } else { |
239 | // XXX: we could get an HTML representation of the content via getParserOutput, but that may |
240 | // contain JS magic and generally may not be suitable for inclusion in a feed. |
241 | // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. |
242 | // Compare also ApiFeedContributions::feedItemDesc |
243 | $html = null; |
244 | } |
245 | |
246 | if ( $html === null ) { |
247 | // Omit large new page diffs, T31110 |
248 | // Also use diff link for non-textual content |
249 | $diffText = self::getDiffLink( $title, $newid ); |
250 | } else { |
251 | $diffText = Html::rawElement( |
252 | 'p', |
253 | [], |
254 | Html::rawElement( 'b', [], wfMessage( 'newpage' )->text() ) |
255 | ); |
256 | $diffText .= Html::rawElement( 'div', [], $html ); |
257 | } |
258 | } |
259 | $completeText .= $diffText; |
260 | |
261 | return $completeText; |
262 | } |
263 | |
264 | /** |
265 | * Generates a diff link. Used when the full diff is not wanted for example |
266 | * when $wgFeedDiffCutoff is 0. |
267 | * |
268 | * @param Title $title Title object: used to generate the diff URL |
269 | * @param int $newid Newid for this diff |
270 | * @param int|null $oldid Oldid for the diff. Null means it is a new article |
271 | * @return string |
272 | */ |
273 | protected static function getDiffLink( Title $title, $newid, $oldid = null ) { |
274 | $queryParameters = [ 'diff' => $newid ]; |
275 | if ( $oldid != null ) { |
276 | $queryParameters['oldid'] = $oldid; |
277 | } |
278 | $diffUrl = $title->getFullURL( $queryParameters ); |
279 | |
280 | $diffLink = Html::element( 'a', [ 'href' => $diffUrl ], |
281 | wfMessage( 'showdiff' )->inContentLanguage()->text() ); |
282 | |
283 | return $diffLink; |
284 | } |
285 | |
286 | /** |
287 | * Hacky application of diff styles for the feeds. |
288 | * Might be 'cleaner' to use DOM or XSLT or something, |
289 | * but *gack* it's a pain in the ass. |
290 | * |
291 | * @param string $text Diff's HTML output |
292 | * @return string Modified HTML |
293 | */ |
294 | public static function applyDiffStyle( $text ) { |
295 | $styles = [ |
296 | 'diff' => 'background-color: #fff; color: #202122;', |
297 | 'diff-otitle' => 'background-color: #fff; color: #202122; text-align: center;', |
298 | 'diff-ntitle' => 'background-color: #fff; color: #202122; text-align: center;', |
299 | 'diff-addedline' => 'color: #202122; font-size: 88%; border-style: solid; ' |
300 | . 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; ' |
301 | . 'vertical-align: top; white-space: pre-wrap;', |
302 | 'diff-deletedline' => 'color: #202122; font-size: 88%; border-style: solid; ' |
303 | . 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; ' |
304 | . 'vertical-align: top; white-space: pre-wrap;', |
305 | 'diff-context' => 'background-color: #f8f9fa; color: #202122; font-size: 88%; ' |
306 | . 'border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; ' |
307 | . 'border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;', |
308 | 'diffchange' => 'font-weight: bold; text-decoration: none;', |
309 | ]; |
310 | |
311 | foreach ( $styles as $class => $style ) { |
312 | $text = preg_replace( '/(<\w+\b[^<>]*)\bclass=([\'"])(?:[^\'"]*\s)?' . |
313 | preg_quote( $class ) . '(?:\s[^\'"]*)?\2(?=[^<>]*>)/', |
314 | '$1style="' . $style . '"', $text ); |
315 | } |
316 | |
317 | return $text; |
318 | } |
319 | |
320 | } |
321 | |
322 | /** @deprecated class alias since 1.40 */ |
323 | class_alias( FeedUtils::class, 'FeedUtils' ); |