Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
58.08% |
327 / 563 |
|
37.50% |
6 / 16 |
CRAP | |
0.00% |
0 / 1 |
RevisionFormatter | |
58.08% |
327 / 563 |
|
37.50% |
6 / 16 |
2566.47 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
setIncludeHistoryProperties | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setIncludeContent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setContentFormat | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
formatApi | |
75.19% |
100 / 133 |
|
0.00% |
0 / 1 |
26.11 | |||
serializeUserLinks | |
83.72% |
36 / 43 |
|
0.00% |
0 / 1 |
5.11 | |||
serializeUser | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
getDateFormats | |
20.00% |
2 / 10 |
|
0.00% |
0 / 1 |
4.05 | |||
buildActions | |
50.38% |
66 / 131 |
|
0.00% |
0 / 1 |
453.90 | |||
buildLinks | |
45.45% |
40 / 88 |
|
0.00% |
0 / 1 |
135.70 | |||
buildProperties | |
15.38% |
2 / 13 |
|
0.00% |
0 / 1 |
13.69 | |||
processParam | |
30.49% |
25 / 82 |
|
0.00% |
0 / 1 |
471.30 | |||
msg | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
decideContentFormat | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
decideTopicTitleContentFormat | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
7 | |||
decideNonTopicTitleContentFormat | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace Flow\Formatter; |
4 | |
5 | use Flow\Collection\PostCollection; |
6 | use Flow\Conversion\Utils; |
7 | use Flow\Exception\FlowException; |
8 | use Flow\Exception\InvalidInputException; |
9 | use Flow\Exception\PermissionException; |
10 | use Flow\Model\AbstractRevision; |
11 | use Flow\Model\Anchor; |
12 | use Flow\Model\PostRevision; |
13 | use Flow\Model\PostSummary; |
14 | use Flow\Model\UUID; |
15 | use Flow\Repository\UserNameBatch; |
16 | use Flow\RevisionActionPermissions; |
17 | use Flow\Templating; |
18 | use Flow\UrlGenerator; |
19 | use MediaWiki\Api\ApiResult; |
20 | use MediaWiki\Cache\GenderCache; |
21 | use MediaWiki\Context\IContextSource; |
22 | use MediaWiki\Logger\LoggerFactory; |
23 | use MediaWiki\MediaWikiServices; |
24 | use MediaWiki\Message\Message; |
25 | use MediaWiki\Registration\ExtensionRegistry; |
26 | use MediaWiki\SpecialPage\SpecialPage; |
27 | use MediaWiki\Title\Title; |
28 | use MediaWiki\User\User; |
29 | use MediaWiki\User\UserGroupManager; |
30 | use MediaWiki\WikiMap\WikiMap; |
31 | use Wikimedia\Message\MessageParam; |
32 | use Wikimedia\Message\ParamType; |
33 | use Wikimedia\Timestamp\TimestampException; |
34 | |
35 | /** |
36 | * This implements a serializer for converting revision objects |
37 | * into an array of localized and sanitized data ready for user |
38 | * consumption. |
39 | * |
40 | * The formatApi method is the primary method of interacting with |
41 | * this serializer. The results of formatApi can be passed on to |
42 | * html formatting or emitted directly as an api response. |
43 | * |
44 | * For performance considerations of special purpose formatters like |
45 | * CheckUser methods that build pieces of the api response are also |
46 | * public. |
47 | * |
48 | * @todo can't output as api yet, Message instances are returned |
49 | * for the various strings. |
50 | * |
51 | * @todo this needs a better name, RevisionSerializer? not sure yet |
52 | */ |
53 | class RevisionFormatter { |
54 | |
55 | /** |
56 | * @var RevisionActionPermissions |
57 | */ |
58 | protected $permissions; |
59 | |
60 | /** |
61 | * @var Templating |
62 | */ |
63 | protected $templating; |
64 | |
65 | /** |
66 | * @var UrlGenerator |
67 | */ |
68 | protected $urlGenerator; |
69 | |
70 | /** |
71 | * @var bool |
72 | */ |
73 | protected $includeProperties = false; |
74 | |
75 | /** |
76 | * @var bool |
77 | */ |
78 | protected $includeContent = true; |
79 | |
80 | /** |
81 | * @var string[] Allowed content formats |
82 | * |
83 | * See setContentFormat. |
84 | */ |
85 | protected $allowedContentFormats = [ 'html', 'wikitext', 'fixed-html', |
86 | 'topic-title-html', 'topic-title-wikitext' ]; |
87 | |
88 | /** |
89 | * @var string Default content format for revision output |
90 | */ |
91 | protected $contentFormat = 'fixed-html'; |
92 | |
93 | /** |
94 | * @var array Map from alphadecimal revision id to content format override |
95 | */ |
96 | protected $revisionContentFormat = []; |
97 | |
98 | /** |
99 | * @var int |
100 | */ |
101 | protected $maxThreadingDepth; |
102 | |
103 | /** |
104 | * @var Message[] |
105 | */ |
106 | protected $messages = []; |
107 | |
108 | /** |
109 | * @var array |
110 | */ |
111 | protected $userLinks = []; |
112 | |
113 | /** |
114 | * @var UserNameBatch |
115 | */ |
116 | protected $usernames; |
117 | |
118 | /** |
119 | * @var GenderCache |
120 | */ |
121 | protected $genderCache; |
122 | |
123 | /** |
124 | * @var UserGroupManager |
125 | */ |
126 | protected $userGroupManager; |
127 | |
128 | /** |
129 | * @param RevisionActionPermissions $permissions |
130 | * @param Templating $templating |
131 | * @param UrlGenerator $urlGenerator |
132 | * @param UserNameBatch $usernames |
133 | * @param int $maxThreadingDepth |
134 | */ |
135 | public function __construct( |
136 | RevisionActionPermissions $permissions, |
137 | Templating $templating, |
138 | UrlGenerator $urlGenerator, |
139 | UserNameBatch $usernames, |
140 | $maxThreadingDepth |
141 | ) { |
142 | $this->permissions = $permissions; |
143 | $this->templating = $templating; |
144 | $this->urlGenerator = $urlGenerator; |
145 | $this->usernames = $usernames; |
146 | $this->genderCache = MediaWikiServices::getInstance()->getGenderCache(); |
147 | $this->userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager(); |
148 | $this->maxThreadingDepth = $maxThreadingDepth; |
149 | } |
150 | |
151 | /** |
152 | * The self::buildProperties method is fairly expensive and only used for rendering |
153 | * history entries. As such it is optimistically disabled unless requested |
154 | * here |
155 | * |
156 | * @param bool $shouldInclude |
157 | */ |
158 | public function setIncludeHistoryProperties( $shouldInclude ) { |
159 | $this->includeProperties = (bool)$shouldInclude; |
160 | } |
161 | |
162 | /** |
163 | * Outputing content can be somehwat expensive, as most of the content is loaded |
164 | * into DOMDocuemnts for processing of relidlinks and badimages. Set this to false |
165 | * if the content will not be used such as for recent changes. |
166 | * @param bool $shouldInclude |
167 | */ |
168 | public function setIncludeContent( $shouldInclude ) { |
169 | $this->includeContent = (bool)$shouldInclude; |
170 | } |
171 | |
172 | /** |
173 | * Sets the content format for all revisions formatted by this formatter, or a |
174 | * particular revision. |
175 | * |
176 | * @param string $format Format to use for revision content. If no revision ID is |
177 | * given, this is a default format, and the allowed formats are 'html', 'wikitext', |
178 | * and 'fixed-html'. |
179 | * |
180 | * For the default format, 'fixed-html' will be converted to 'topic-title-html' |
181 | * when formatting a topic title. 'html' and 'wikitext' will be converted to |
182 | * 'topic-title-wikitext' for topic titles (because 'html' and 'wikitext' are |
183 | * editable, and 'topic-title-html' is not editable). |
184 | * |
185 | * If a revision ID is given, the allowed formats are 'html', 'wikitext', |
186 | * 'fixed-html', 'topic-title-html', and 'topic-title-wikitext'. However, the |
187 | * format will not be converted, and must be valid for the given revision ('html', |
188 | * 'wikitext', and 'fixed-html' are valid only for non-topic titles. |
189 | * 'topic-title-html' and 'topic-title-wikitext' are only valid for topic titles. |
190 | * Otherwise, an exception will be thrown later. |
191 | * @param UUID|null $revisionId Revision ID this format applies for. |
192 | * @throws FlowException |
193 | * @throws InvalidInputException |
194 | */ |
195 | public function setContentFormat( $format, ?UUID $revisionId = null ) { |
196 | if ( !in_array( $format, $this->allowedContentFormats ) ) { |
197 | throw new InvalidInputException( "Unknown content format: $format" ); |
198 | } |
199 | if ( $revisionId === null ) { |
200 | // set default content format |
201 | $this->contentFormat = $format; |
202 | } else { |
203 | // set per-revision content format |
204 | $this->revisionContentFormat[$revisionId->getAlphadecimal()] = $format; |
205 | } |
206 | } |
207 | |
208 | /** |
209 | * @param FormatterRow $row |
210 | * @param IContextSource $ctx |
211 | * @param string $action action from FlowActions |
212 | * @return array|bool |
213 | * @throws FlowException |
214 | * @throws PermissionException |
215 | * @throws \Exception |
216 | * @throws \Flow\Exception\InvalidInputException |
217 | * @throws TimestampException |
218 | * @suppress PhanUndeclaredMethod Phan doesn't infer types from the instanceofs |
219 | */ |
220 | public function formatApi( FormatterRow $row, IContextSource $ctx, $action = 'view' ) { |
221 | $this->permissions->setUser( $ctx->getUser() ); |
222 | |
223 | if ( !$this->permissions->isAllowed( $row->revision, $action ) ) { |
224 | LoggerFactory::getInstance( 'Flow' )->debug( |
225 | __METHOD__ . ': Permission denied for user on action {action}', |
226 | [ |
227 | 'action' => $action, |
228 | 'revision_id' => $row->revision->getRevisionId()->getAlphadecimal(), |
229 | 'user_id' => $ctx->getUser()->getId(), |
230 | ] |
231 | ); |
232 | return false; |
233 | } |
234 | |
235 | $moderatedRevision = $this->templating->getModeratedRevision( $row->revision ); |
236 | $ts = $row->revision->getRevisionId()->getTimestampObj(); |
237 | $res = [ |
238 | ApiResult::META_BC_BOOLS => [ |
239 | 'isOriginalContent', |
240 | 'isModerated', |
241 | 'isLocked', |
242 | 'isModeratedNotLocked', |
243 | ], |
244 | 'workflowId' => $row->workflow->getId()->getAlphadecimal(), |
245 | 'articleTitle' => $row->workflow->getArticleTitle()->getPrefixedText(), |
246 | 'revisionId' => $row->revision->getRevisionId()->getAlphadecimal(), |
247 | 'timestamp' => $ts->getTimestamp( TS_MW ), |
248 | 'changeType' => $row->revision->getChangeType(), |
249 | // @todo push all date formatting to the render side? |
250 | 'dateFormats' => $this->getDateFormats( $row->revision, $ctx ), |
251 | 'properties' => $this->buildProperties( $row->workflow->getId(), $row->revision, $ctx, $row ), |
252 | 'isOriginalContent' => $row->revision->isOriginalContent(), |
253 | 'isModerated' => $moderatedRevision->isModerated(), |
254 | // These are read urls |
255 | 'links' => $this->buildLinks( $row ), |
256 | // These are write urls |
257 | 'actions' => $this->buildActions( $row ), |
258 | 'size' => [ |
259 | 'old' => $row->revision->getPreviousContentLength(), |
260 | 'new' => $row->revision->getContentLength(), |
261 | ], |
262 | 'author' => $this->serializeUser( |
263 | $row->revision->getUserWiki(), |
264 | $row->revision->getUserId(), |
265 | $row->revision->getUserIp() |
266 | ), |
267 | 'lastEditUser' => $this->serializeUser( |
268 | $row->revision->getLastContentEditUserWiki(), |
269 | $row->revision->getLastContentEditUserId(), |
270 | $row->revision->getLastContentEditUserIp() |
271 | ), |
272 | 'lastEditId' => $row->revision->isOriginalContent() |
273 | ? null : $row->revision->getLastContentEditId()->getAlphadecimal(), |
274 | 'previousRevisionId' => $row->revision->isFirstRevision() |
275 | ? null |
276 | : $row->revision->getPrevRevisionId()->getAlphadecimal(), |
277 | ]; |
278 | |
279 | if ( $res['isModerated'] ) { |
280 | $res['moderator'] = $this->serializeUser( |
281 | $moderatedRevision->getModeratedByUserWiki(), |
282 | $moderatedRevision->getModeratedByUserId(), |
283 | $moderatedRevision->getModeratedByUserIp() |
284 | ); |
285 | // @todo why moderate instead of moderated or something else? |
286 | $res['moderateState'] = $moderatedRevision->getModerationState(); |
287 | $res['moderateReason'] = [ |
288 | 'content' => $moderatedRevision->getModeratedReason(), |
289 | 'format' => 'plaintext', |
290 | ]; |
291 | $res['isLocked'] = $moderatedRevision->isLocked(); |
292 | } else { |
293 | $res['isLocked'] = false; |
294 | } |
295 | // to avoid doing this check in handlebars |
296 | $res['isModeratedNotLocked'] = $moderatedRevision->isModerated() && !$moderatedRevision->isLocked(); |
297 | |
298 | if ( $this->includeContent ) { |
299 | $contentFormat = $this->decideContentFormat( $row->revision ); |
300 | |
301 | // @todo better name? |
302 | $res['content'] = [ |
303 | 'content' => $this->templating->getContent( $row->revision, $contentFormat ), |
304 | 'format' => $contentFormat |
305 | ]; |
306 | } |
307 | |
308 | if ( $row instanceof TopicRow ) { |
309 | $res[ApiResult::META_BC_BOOLS] = array_merge( |
310 | $res[ApiResult::META_BC_BOOLS], |
311 | [ |
312 | 'isWatched', |
313 | 'watchable', |
314 | ] |
315 | ); |
316 | if ( $row->summary ) { |
317 | $summary = $this->formatApi( $row->summary, $ctx, $action ); |
318 | if ( $summary ) { |
319 | $res['summary'] = [ |
320 | 'revision' => $summary, |
321 | ]; |
322 | } |
323 | } |
324 | |
325 | // Only non-anon users can watch/unwatch a flow topic |
326 | // isWatched - the topic is watched by current user |
327 | // watchable - the user could watch the topic, eg, anon-user can't watch a topic |
328 | if ( $ctx->getUser()->isRegistered() ) { |
329 | // default topic is not watched and topic is not always watched |
330 | $res['isWatched'] = (bool)$row->isWatched; |
331 | $res['watchable'] = true; |
332 | } else { |
333 | $res['watchable'] = false; |
334 | } |
335 | } |
336 | |
337 | if ( $row->revision instanceof PostRevision ) { |
338 | $res[ApiResult::META_BC_BOOLS] = array_merge( |
339 | $res[ApiResult::META_BC_BOOLS], |
340 | [ |
341 | 'isMaxThreadingDepth', |
342 | 'isNewPage', |
343 | ] |
344 | ); |
345 | |
346 | $replyTo = $row->revision->getReplyToId(); |
347 | $res['replyToId'] = $replyTo ? $replyTo->getAlphadecimal() : null; |
348 | $res['postId'] = $row->revision->getPostId()->getAlphadecimal(); |
349 | $res['isMaxThreadingDepth'] = $row->revision->getDepth() >= $this->maxThreadingDepth; |
350 | $res['creator'] = $this->serializeUser( |
351 | $row->revision->getCreatorWiki(), |
352 | $row->revision->getCreatorId(), |
353 | $row->revision->getCreatorIp() |
354 | ); |
355 | |
356 | // Always output this along with topic titles so they |
357 | // have a safe parameter to use within l10n for content |
358 | // output. |
359 | if ( $row->revision->isTopicTitle() && !isset( $res['properties']['topic-of-post'] ) ) { |
360 | $res['properties']['topic-of-post'] = $this->processParam( |
361 | 'topic-of-post', |
362 | $row->revision, |
363 | $row->workflow->getId(), |
364 | $ctx, |
365 | $row |
366 | ); |
367 | |
368 | $res['properties']['topic-of-post-text-from-html'] = $this->processParam( |
369 | 'topic-of-post-text-from-html', |
370 | $row->revision, |
371 | $row->workflow->getId(), |
372 | $ctx, |
373 | $row |
374 | ); |
375 | |
376 | // moderated posts won't have that property |
377 | // FIXME: This shouldn't depend on Message implementation details. |
378 | // You're not really supposed to know how Message represents the |
379 | // parameters internally. |
380 | if ( |
381 | isset( $res['properties']['topic-of-post-text-from-html'] ) && |
382 | $res['properties']['topic-of-post-text-from-html'] instanceof MessageParam && |
383 | $res['properties']['topic-of-post-text-from-html']->getType() === ParamType::PLAINTEXT |
384 | ) { |
385 | $res['content']['plaintext'] = |
386 | $res['properties']['topic-of-post-text-from-html']->getValue(); |
387 | } |
388 | } |
389 | |
390 | $res['isNewPage'] = $row->isFirstReply && $row->revision->isFirstRevision(); |
391 | |
392 | } elseif ( $row->revision instanceof PostSummary ) { |
393 | $res['creator'] = $this->serializeUser( |
394 | $row->revision->getCreatorWiki(), |
395 | $row->revision->getCreatorId(), |
396 | $row->revision->getCreatorIp() |
397 | ); |
398 | } |
399 | |
400 | return $res; |
401 | } |
402 | |
403 | /** |
404 | * @param array $userData Contains `name`, `wiki`, and `gender` keys |
405 | * @return array |
406 | */ |
407 | public function serializeUserLinks( $userData ) { |
408 | $name = $userData['name']; |
409 | if ( isset( $this->userLinks[$name] ) ) { |
410 | return $this->userLinks[$name]; |
411 | } |
412 | |
413 | $talkPageTitle = null; |
414 | $userTitle = Title::newFromText( $name, NS_USER ); |
415 | if ( $userTitle ) { |
416 | $talkPageTitle = $userTitle->getTalkPage(); |
417 | } |
418 | |
419 | $blockTitle = SpecialPage::getTitleFor( 'Block', $name ); |
420 | |
421 | $userContribsTitle = SpecialPage::getTitleFor( 'Contributions', $name ); |
422 | $userLinksBCBools = [ |
423 | '_BC_bools' => [ |
424 | 'exists', |
425 | ], |
426 | ]; |
427 | $links = [ |
428 | 'contribs' => [ |
429 | 'url' => $userContribsTitle->getLinkURL(), |
430 | 'title' => $userContribsTitle->getText(), |
431 | 'exists' => true, |
432 | ] + $userLinksBCBools, |
433 | 'userpage' => [ |
434 | 'url' => $userTitle->getLinkURL(), |
435 | 'title' => $userTitle->getText(), |
436 | 'exists' => $userTitle->isKnown(), |
437 | ] + $userLinksBCBools, |
438 | ]; |
439 | |
440 | if ( $talkPageTitle ) { |
441 | $links['talk'] = [ |
442 | 'url' => $talkPageTitle->getLinkURL(), |
443 | 'title' => $talkPageTitle->getPrefixedText(), |
444 | 'exists' => $talkPageTitle->isKnown() |
445 | ] + $userLinksBCBools; |
446 | } |
447 | // is this right permissions? typically this would |
448 | // be sourced from Linker::userToolLinks, but that |
449 | // only undertands html strings. |
450 | if ( MediaWikiServices::getInstance()->getPermissionManager() |
451 | ->userHasRight( $this->permissions->getUser(), 'block' ) |
452 | ) { |
453 | // only is the user has blocking rights |
454 | $links += [ |
455 | "block" => [ |
456 | 'url' => $blockTitle->getLinkURL(), |
457 | 'title' => wfMessage( 'blocklink' ), |
458 | 'exists' => true |
459 | ] + $userLinksBCBools, |
460 | ]; |
461 | } |
462 | |
463 | $this->userLinks[$name] = $links; |
464 | return $this->userLinks[$name]; |
465 | } |
466 | |
467 | public function serializeUser( $userWiki, $userId, $userIp ) { |
468 | $res = [ |
469 | 'name' => $this->usernames->get( $userWiki, $userId, $userIp ), |
470 | 'wiki' => $userWiki, |
471 | 'gender' => 'unknown', |
472 | 'links' => [], |
473 | 'id' => $userId |
474 | ]; |
475 | // Only works for the local wiki |
476 | if ( $res['name'] && WikiMap::getCurrentWikiId() === $userWiki ) { |
477 | $res['gender'] = $this->genderCache->getGenderOf( $res['name'], __METHOD__ ); |
478 | } |
479 | if ( $res['name'] ) { |
480 | $res['links'] = $this->serializeUserLinks( $res ); |
481 | } |
482 | |
483 | return $res; |
484 | } |
485 | |
486 | /** |
487 | * @param AbstractRevision $revision |
488 | * @param IContextSource $ctx |
489 | * @return string[] Contains [timeAndDate, date, time] |
490 | */ |
491 | public function getDateFormats( AbstractRevision $revision, IContextSource $ctx ) { |
492 | // also restricted to history |
493 | if ( $this->includeProperties === false ) { |
494 | return []; |
495 | } |
496 | |
497 | $timestamp = $revision->getRevisionId()->getTimestampObj()->getTimestamp( TS_MW ); |
498 | $user = $ctx->getUser(); |
499 | $lang = $ctx->getLanguage(); |
500 | |
501 | return [ |
502 | 'timeAndDate' => $lang->userTimeAndDate( $timestamp, $user ), |
503 | 'date' => $lang->userDate( $timestamp, $user ), |
504 | 'time' => $lang->userTime( $timestamp, $user ), |
505 | ]; |
506 | } |
507 | |
508 | /** |
509 | * @param FormatterRow $row |
510 | * @return array |
511 | * @throws FlowException |
512 | */ |
513 | public function buildActions( FormatterRow $row ) { |
514 | global $wgThanksSendToBots; |
515 | |
516 | $user = $this->permissions->getUser(); |
517 | $workflow = $row->workflow; |
518 | $title = $workflow->getArticleTitle(); |
519 | |
520 | // If a user does not have rights to perform actions on this page return |
521 | // an empty array of actions. |
522 | if ( !$workflow->userCan( 'edit', $user ) ) { |
523 | return []; |
524 | } |
525 | |
526 | $revision = $row->revision; |
527 | $action = $revision->getChangeType(); |
528 | $workflowId = $workflow->getId(); |
529 | $revId = $revision->getRevisionId(); |
530 | // @phan-suppress-next-line PhanUndeclaredMethod Checks method_exists |
531 | $postId = method_exists( $revision, 'getPostId' ) ? $revision->getPostId() : null; |
532 | $actionTypes = $this->permissions->getActions()->getValue( $action, 'actions' ); |
533 | if ( $actionTypes === null ) { |
534 | wfDebugLog( 'Flow', __METHOD__ . ": No actions defined for action: $action" ); |
535 | return []; |
536 | } |
537 | |
538 | // actions primarily vary by revision type... |
539 | $links = []; |
540 | foreach ( $actionTypes as $type ) { |
541 | if ( !$this->permissions->isAllowed( $revision, $type ) ) { |
542 | continue; |
543 | } |
544 | switch ( $type ) { |
545 | case 'thank': |
546 | $targetedUser = User::newFromId( $revision->getCreatorId() ); |
547 | if ( |
548 | // thanks extension must be available |
549 | ExtensionRegistry::getInstance()->isLoaded( 'Thanks' ) && |
550 | // anons can't give a thank |
551 | $user->isRegistered() && |
552 | // can only thank for PostRevisions |
553 | // (other revision objects have no getCreator* methods) |
554 | $revision instanceof PostRevision && |
555 | // only thank a logged in user |
556 | $targetedUser->isRegistered() && |
557 | // can't thank self |
558 | $user->getId() !== $revision->getCreatorId() && |
559 | // can't thank bots |
560 | !( !$wgThanksSendToBots && in_array( 'bot', $this->userGroupManager->getUserGroups( $targetedUser ) ) ) |
561 | ) { |
562 | $links['thank'] = $this->urlGenerator->thankAction( $postId ); |
563 | } |
564 | break; |
565 | |
566 | case 'reply': |
567 | if ( !$postId ) { |
568 | throw new FlowException( "$type called without \$postId" ); |
569 | } elseif ( !$revision instanceof PostRevision ) { |
570 | throw new FlowException( "$type called without PostRevision object" ); |
571 | } |
572 | |
573 | /* |
574 | * If the post being replied to is the most recent post |
575 | * of its depth, the reply link should point to parent |
576 | */ |
577 | $replyToId = $postId; |
578 | $replyToRevision = $revision; |
579 | if ( $row->isLastReply ) { |
580 | $replyToId = $replyToRevision->getReplyToId(); |
581 | $replyToRevision = PostCollection::newFromId( $replyToId )->getLastRevision(); |
582 | } |
583 | |
584 | /* |
585 | * If the post being replied to is at or exceeds the max |
586 | * threading depth, the reply link should point to parent. |
587 | */ |
588 | while ( $replyToRevision->getDepth() >= $this->maxThreadingDepth ) { |
589 | $replyToId = $replyToRevision->getReplyToId(); |
590 | $replyToRevision = PostCollection::newFromId( $replyToId )->getLastRevision(); |
591 | } |
592 | |
593 | $links['reply'] = $this->urlGenerator->replyAction( |
594 | $title, |
595 | $workflowId, |
596 | $replyToId, |
597 | $revision->isTopicTitle() |
598 | ); |
599 | break; |
600 | |
601 | case 'edit-header': |
602 | $links['edit'] = $this->urlGenerator->editHeaderAction( $title, $workflowId, $revId ); |
603 | break; |
604 | |
605 | case 'edit-title': |
606 | if ( !$postId ) { |
607 | throw new FlowException( "$type called without \$postId" ); |
608 | } |
609 | $links['edit'] = $this->urlGenerator |
610 | ->editTitleAction( $title, $workflowId, $postId, $revId ); |
611 | break; |
612 | |
613 | case 'edit-post': |
614 | if ( !$postId ) { |
615 | throw new FlowException( "$type called without \$postId" ); |
616 | } |
617 | $links['edit'] = $this->urlGenerator |
618 | ->editPostAction( $title, $workflowId, $postId, $revId ); |
619 | break; |
620 | |
621 | case 'undo-edit-header': |
622 | case 'undo-edit-post': |
623 | case 'undo-edit-topic-summary': |
624 | if ( !$revision->isFirstRevision() ) { |
625 | $links['undo'] = $this->urlGenerator->undoAction( $revision, $title, $workflowId ); |
626 | } |
627 | break; |
628 | |
629 | case 'hide-post': |
630 | if ( !$postId ) { |
631 | throw new FlowException( "$type called without \$postId" ); |
632 | } |
633 | $links['hide'] = $this->urlGenerator->hidePostAction( $title, $workflowId, $postId ); |
634 | break; |
635 | |
636 | case 'delete-topic': |
637 | $links['delete'] = $this->urlGenerator->deleteTopicAction( $title, $workflowId ); |
638 | break; |
639 | |
640 | case 'delete-post': |
641 | if ( !$postId ) { |
642 | throw new FlowException( "$type called without \$postId" ); |
643 | } |
644 | $links['delete'] = $this->urlGenerator->deletePostAction( $title, $workflowId, $postId ); |
645 | break; |
646 | |
647 | case 'suppress-topic': |
648 | $links['suppress'] = $this->urlGenerator->suppressTopicAction( $title, $workflowId ); |
649 | break; |
650 | |
651 | case 'suppress-post': |
652 | if ( !$postId ) { |
653 | throw new FlowException( "$type called without \$postId" ); |
654 | } |
655 | $links['suppress'] = $this->urlGenerator->suppressPostAction( $title, $workflowId, $postId ); |
656 | break; |
657 | |
658 | case 'lock-topic': |
659 | // lock topic link is only available to topics |
660 | if ( !$revision instanceof PostRevision || !$revision->isTopicTitle() ) { |
661 | break; |
662 | } |
663 | |
664 | $links['lock'] = $this->urlGenerator->lockTopicAction( $title, $workflowId ); |
665 | break; |
666 | |
667 | case 'restore-topic': |
668 | $moderateAction = $flowAction = null; |
669 | switch ( $revision->getModerationState() ) { |
670 | case AbstractRevision::MODERATED_LOCKED: |
671 | $moderateAction = 'unlock'; |
672 | $flowAction = 'lock-topic'; |
673 | break; |
674 | case AbstractRevision::MODERATED_HIDDEN: |
675 | case AbstractRevision::MODERATED_DELETED: |
676 | case AbstractRevision::MODERATED_SUPPRESSED: |
677 | $moderateAction = 'un' . $revision->getModerationState(); |
678 | $flowAction = 'moderate-topic'; |
679 | break; |
680 | } |
681 | if ( $moderateAction && $flowAction ) { |
682 | $links[$moderateAction] = $this->urlGenerator->restoreTopicAction( |
683 | $title, $workflowId, $moderateAction, $flowAction ); |
684 | } |
685 | break; |
686 | |
687 | case 'restore-post': |
688 | if ( !$postId ) { |
689 | throw new FlowException( "$type called without \$postId" ); |
690 | } |
691 | $moderateAction = $flowAction = null; |
692 | switch ( $revision->getModerationState() ) { |
693 | case AbstractRevision::MODERATED_HIDDEN: |
694 | case AbstractRevision::MODERATED_DELETED: |
695 | case AbstractRevision::MODERATED_SUPPRESSED: |
696 | $moderateAction = 'un' . $revision->getModerationState(); |
697 | $flowAction = 'moderate-post'; |
698 | break; |
699 | } |
700 | if ( $moderateAction && $flowAction ) { |
701 | $links[$moderateAction] = $this->urlGenerator->restorePostAction( |
702 | $title, $workflowId, $postId, $moderateAction, $flowAction ); |
703 | } |
704 | break; |
705 | |
706 | case 'hide-topic': |
707 | $links['hide'] = $this->urlGenerator->hideTopicAction( $title, $workflowId ); |
708 | break; |
709 | |
710 | // Need to use 'edit-topic-summary' to match FlowActions |
711 | case 'edit-topic-summary': |
712 | // summarize link is only available to topic workflow |
713 | if ( !in_array( $workflow->getType(), [ 'topic', 'topicsummary' ] ) ) { |
714 | break; |
715 | } |
716 | $links['summarize'] = $this->urlGenerator->editTopicSummaryAction( $title, $workflowId ); |
717 | break; |
718 | |
719 | default: |
720 | wfDebugLog( 'Flow', __METHOD__ . ': unkown action link type: ' . $type ); |
721 | break; |
722 | } |
723 | } |
724 | |
725 | return $links; |
726 | } |
727 | |
728 | /** |
729 | * @param FormatterRow $row |
730 | * @return Anchor[] |
731 | * @throws FlowException |
732 | */ |
733 | public function buildLinks( FormatterRow $row ) { |
734 | $workflow = $row->workflow; |
735 | $revision = $row->revision; |
736 | $title = $workflow->getArticleTitle(); |
737 | $action = $revision->getChangeType(); |
738 | $workflowId = $workflow->getId(); |
739 | $revId = $revision->getRevisionId(); |
740 | // @phan-suppress-next-line PhanUndeclaredMethod Checks method_exists |
741 | $postId = method_exists( $revision, 'getPostId' ) ? $revision->getPostId() : null; |
742 | |
743 | $linkTypes = $this->permissions->getActions()->getValue( $action, 'links' ); |
744 | if ( $linkTypes === null ) { |
745 | wfDebugLog( 'Flow', __METHOD__ . ": No links defined for action: $action" ); |
746 | return []; |
747 | } |
748 | |
749 | $links = []; |
750 | $diffCallback = null; |
751 | foreach ( $linkTypes as $type ) { |
752 | switch ( $type ) { |
753 | case 'watch-topic': |
754 | $links['watch-topic'] = $this->urlGenerator->watchTopicLink( $title, $workflowId ); |
755 | break; |
756 | |
757 | case 'unwatch-topic': |
758 | $links['unwatch-topic'] = $this->urlGenerator->unwatchTopicLink( $title, $workflowId ); |
759 | break; |
760 | |
761 | case 'topic': |
762 | $links['topic'] = $this->urlGenerator->topicLink( $title, $workflowId ); |
763 | break; |
764 | |
765 | case 'post': |
766 | if ( !$postId ) { |
767 | wfDebugLog( 'Flow', __METHOD__ . ': No postId available to render post link' ); |
768 | break; |
769 | } |
770 | $links['post'] = $this->urlGenerator->postLink( $title, $workflowId, $postId ); |
771 | break; |
772 | |
773 | case 'header-revision': |
774 | $links['header-revision'] = $this->urlGenerator |
775 | ->headerRevisionLink( $title, $workflowId, $revId ); |
776 | break; |
777 | |
778 | case 'topic-revision': |
779 | if ( !$postId ) { |
780 | wfDebugLog( 'Flow', __METHOD__ . ': No postId available to render revision link' ); |
781 | break; |
782 | } |
783 | |
784 | $links['topic-revision'] = $this->urlGenerator |
785 | ->topicRevisionLink( $title, $workflowId, $revId ); |
786 | break; |
787 | |
788 | case 'post-revision': |
789 | if ( !$postId ) { |
790 | wfDebugLog( 'Flow', __METHOD__ . ': No postId available to render revision link' ); |
791 | break; |
792 | } |
793 | |
794 | $links['post-revision'] = $this->urlGenerator |
795 | ->postRevisionLink( $title, $workflowId, $postId, $revId ); |
796 | break; |
797 | |
798 | case 'summary-revision': |
799 | $links['summary-revision'] = $this->urlGenerator |
800 | ->summaryRevisionLink( $title, $workflowId, $revId ); |
801 | break; |
802 | |
803 | case 'post-history': |
804 | if ( !$postId ) { |
805 | wfDebugLog( 'Flow', __METHOD__ . ': No postId available to render post-history link' ); |
806 | break; |
807 | } |
808 | $links['post-history'] = $this->urlGenerator->postHistoryLink( $title, $workflowId, $postId ); |
809 | break; |
810 | |
811 | case 'topic-history': |
812 | $links['topic-history'] = $this->urlGenerator->workflowHistoryLink( $title, $workflowId ); |
813 | break; |
814 | |
815 | case 'board-history': |
816 | $links['board-history'] = $this->urlGenerator->boardHistoryLink( $title ); |
817 | break; |
818 | |
819 | case 'diff-header': |
820 | $diffCallback ??= [ $this->urlGenerator, 'diffHeaderLink' ]; |
821 | // don't break, diff links are rendered below |
822 | case 'diff-post': |
823 | $diffCallback ??= [ $this->urlGenerator, 'diffPostLink' ]; |
824 | // don't break, diff links are rendered below |
825 | case 'diff-post-summary': |
826 | $diffCallback ??= [ $this->urlGenerator, 'diffSummaryLink' ]; |
827 | |
828 | /* |
829 | * To diff against previous revision, we don't really need that |
830 | * revision id; if no particular diff id is specified, it will |
831 | * assume a diff against previous revision. However, we do want |
832 | * to make sure that a previous revision actually exists to diff |
833 | * against. This could result in a network request (fetching the |
834 | * current revision), but it's likely being loaded anyways. |
835 | */ |
836 | if ( $revision->getPrevRevisionId() !== null ) { |
837 | $links['diff'] = $diffCallback( $title, $workflowId, $revId ); |
838 | |
839 | /* |
840 | * Different formatters have different terminology for the link |
841 | * that diffs a certain revision to the previous revision. |
842 | * |
843 | * E.g.: Special:Contributions has "diff" ($links['diff']), |
844 | * ?action=history has "prev" ($links['prev']). |
845 | */ |
846 | $links['diff-prev'] = clone $links['diff']; |
847 | $lastMsg = new Message( 'last' ); |
848 | $links['diff-prev']->setTitleMessage( $lastMsg ); |
849 | $links['diff-prev']->setMessage( $lastMsg ); |
850 | } |
851 | |
852 | /* |
853 | * To diff against the current revision, we need to know the id |
854 | * of this last revision. This could be an additional network |
855 | * request, though anything using formatter likely already needs |
856 | * to request the most current revision (e.g. to check |
857 | * permissions) so we should be able to get it from local cache. |
858 | */ |
859 | $cur = $row->currentRevision; |
860 | if ( !$revId->equals( $cur->getRevisionId() ) ) { |
861 | $links['diff-cur'] = $diffCallback( $title, $workflowId, $cur->getRevisionId(), $revId ); |
862 | $curMsg = new Message( 'cur' ); |
863 | $links['diff-cur']->setTitleMessage( $curMsg ); |
864 | $links['diff-cur']->setMessage( $curMsg ); |
865 | } |
866 | break; |
867 | |
868 | case 'workflow': |
869 | $links['workflow'] = $this->urlGenerator->workflowLink( $title, $workflowId ); |
870 | break; |
871 | |
872 | default: |
873 | wfDebugLog( 'Flow', __METHOD__ . ': unkown action link type: ' . $type ); |
874 | break; |
875 | } |
876 | } |
877 | |
878 | return $links; |
879 | } |
880 | |
881 | /** |
882 | * Build api properties defined in FlowActions for this change type |
883 | * |
884 | * This is a fairly expensive function(compared to the other methods in this class). |
885 | * As such its only output when specifically requested |
886 | * |
887 | * @param UUID $workflowId |
888 | * @param AbstractRevision $revision |
889 | * @param IContextSource $ctx |
890 | * @param FormatterRow|null $row |
891 | * @return array |
892 | */ |
893 | public function buildProperties( |
894 | UUID $workflowId, |
895 | AbstractRevision $revision, |
896 | IContextSource $ctx, |
897 | ?FormatterRow $row = null |
898 | ) { |
899 | if ( $this->includeProperties === false ) { |
900 | return []; |
901 | } |
902 | |
903 | $changeType = $revision->getChangeType(); |
904 | $actions = $this->permissions->getActions(); |
905 | $params = $actions->getValue( $changeType, 'history', 'i18n-params' ); |
906 | if ( !$params ) { |
907 | // should we have a sigil for i18n with no parameters? |
908 | wfDebugLog( 'Flow', __METHOD__ . ": No i18n params for changeType $changeType on " . |
909 | $revision->getRevisionId()->getAlphadecimal() ); |
910 | return []; |
911 | } |
912 | |
913 | $res = [ '_key' => $actions->getValue( $changeType, 'history', 'i18n-message' ) ]; |
914 | foreach ( $params as $param ) { |
915 | $res[$param] = $this->processParam( $param, $revision, $workflowId, $ctx, $row ); |
916 | } |
917 | |
918 | return $res; |
919 | } |
920 | |
921 | /** |
922 | * Mimic Echo parameter formatting |
923 | * |
924 | * @param string $param The requested i18n parameter |
925 | * @param AbstractRevision|AbstractRevision[] $revision The revision or |
926 | * revisions to format or an array of revisions |
927 | * @param UUID $workflowId The UUID of the workflow $revision belongs tow |
928 | * @param IContextSource $ctx |
929 | * @param FormatterRow|null $row |
930 | * @return string|MessageParam A valid parameter for a core Message instance. These |
931 | * parameters will be used with Message::parse |
932 | * @throws FlowException |
933 | */ |
934 | public function processParam( |
935 | $param, |
936 | $revision, |
937 | UUID $workflowId, |
938 | IContextSource $ctx, |
939 | ?FormatterRow $row = null |
940 | ) { |
941 | $isWikiText = str_ends_with( $param, 'wikitext' ); |
942 | $format = $isWikiText ? $revision->getWikitextFormat() : $revision->getHtmlFormat(); |
943 | |
944 | switch ( $param ) { |
945 | case 'creator-text': |
946 | if ( $revision instanceof PostRevision ) { |
947 | return $this->usernames->getFromTuple( $revision->getCreatorTuple() ); |
948 | } else { |
949 | return ''; |
950 | } |
951 | |
952 | case 'user-text': |
953 | return $this->usernames->getFromTuple( $revision->getUserTuple() ); |
954 | |
955 | case 'user-links': |
956 | return Message::rawParam( $this->templating->getUserLinks( $revision ) ); |
957 | |
958 | case 'summary': |
959 | if ( !$this->permissions->isAllowed( $revision, 'view' ) ) { |
960 | return ''; |
961 | } |
962 | |
963 | /* |
964 | * Fetch in HTML; unparsed wikitext in summary is pointless. |
965 | * Larger-scale wikis will likely also store content in html, so no |
966 | * Parsoid roundtrip is needed then (and if it *is*, it'll already |
967 | * be needed to render Flow discussions, so this is manageable) |
968 | */ |
969 | $content = $this->templating->getContent( $revision, 'fixed-html' ); |
970 | // strip html tags and decode to plaintext |
971 | $content = Utils::htmlToPlaintext( $content, 140, $ctx->getLanguage() ); |
972 | return Message::plaintextParam( $content ); |
973 | |
974 | case 'wikitext': |
975 | case 'plaintext': |
976 | if ( !$this->permissions->isAllowed( $revision, 'view' ) ) { |
977 | return ''; |
978 | } |
979 | |
980 | $content = $this->templating->getContent( $revision, $format ); |
981 | if ( !$isWikiText ) { |
982 | $content = Utils::htmlToPlaintext( $content ); |
983 | } |
984 | // This must be escaped and marked raw to prevent special chars in |
985 | // content, like $1, from changing the i18n result |
986 | return Message::plaintextParam( $content ); |
987 | |
988 | // This is potentially two networked round trips, much too expensive for |
989 | // the rendering loop |
990 | case 'prev-wikitext': |
991 | case 'prev-plaintext': |
992 | if ( $revision->isFirstRevision() ) { |
993 | return ''; |
994 | } |
995 | if ( $row === null ) { |
996 | $previousRevision = $revision->getCollection()->getPrevRevision( $revision ); |
997 | } else { |
998 | $previousRevision = $row->previousRevision; |
999 | } |
1000 | if ( !$previousRevision ) { |
1001 | return ''; |
1002 | } |
1003 | if ( !$this->permissions->isAllowed( $previousRevision, 'view' ) ) { |
1004 | return ''; |
1005 | } |
1006 | |
1007 | $content = $this->templating->getContent( $previousRevision, $format ); |
1008 | if ( !$isWikiText ) { |
1009 | $content = Utils::htmlToPlaintext( $content ); |
1010 | } |
1011 | return Message::plaintextParam( $content ); |
1012 | |
1013 | case 'workflow-url': |
1014 | return $this->urlGenerator |
1015 | ->workflowLink( null, $workflowId ) |
1016 | ->getFullURL(); |
1017 | |
1018 | case 'post-url': |
1019 | if ( !$revision instanceof PostRevision ) { |
1020 | throw new FlowException( 'Expected PostRevision but received' . get_class( $revision ) ); |
1021 | } |
1022 | return $this->urlGenerator |
1023 | ->postLink( null, $workflowId, $revision->getPostId() ) |
1024 | ->getFullURL(); |
1025 | |
1026 | case 'moderated-reason': |
1027 | // don't parse wikitext in the moderation reason |
1028 | return Message::plaintextParam( $revision->getModeratedReason() ?? '' ); |
1029 | |
1030 | case 'topic-of-post': |
1031 | if ( !$revision instanceof PostRevision ) { |
1032 | throw new FlowException( 'Expected PostRevision but received ' . get_class( $revision ) ); |
1033 | } |
1034 | |
1035 | $root = $revision->getRootPost(); |
1036 | if ( !$this->permissions->isAllowed( $root, 'view-topic-title' ) ) { |
1037 | return ''; |
1038 | } |
1039 | |
1040 | $content = $this->templating->getContent( $root, 'topic-title-wikitext' ); |
1041 | |
1042 | // TODO: We need to use plaintextParam or similar to avoid parsing, |
1043 | // but the API output says "plaintext", which is confusing and |
1044 | // should be fixed. From the API consumer's perspective, it's |
1045 | // topic-title-wikitext. |
1046 | return Message::plaintextParam( $content ); |
1047 | |
1048 | // Strip the tags from the HTML version to produce text: |
1049 | // [[Red link 3]], [[Adrines]], [[Media:Earth.jpg]], http://example.com => |
1050 | // Red link 3, Adrines, Media:Earth.jpg, http://example.com |
1051 | case 'topic-of-post-text-from-html': |
1052 | if ( !$revision instanceof PostRevision ) { |
1053 | throw new FlowException( 'Expected PostRevision but received ' . get_class( $revision ) ); |
1054 | } |
1055 | |
1056 | $root = $revision->getRootPost(); |
1057 | if ( !$this->permissions->isAllowed( $root, 'view-topic-title' ) ) { |
1058 | return ''; |
1059 | } |
1060 | |
1061 | $content = $this->templating->getContent( $root, 'topic-title-plaintext' ); |
1062 | |
1063 | return Message::plaintextParam( $content ); |
1064 | |
1065 | case 'post-of-summary': |
1066 | if ( !$revision instanceof PostSummary ) { |
1067 | throw new FlowException( 'Expected PostSummary but received ' . get_class( $revision ) ); |
1068 | } |
1069 | |
1070 | /** @var PostRevision $post */ |
1071 | $post = $revision->getCollection()->getPost()->getLastRevision(); |
1072 | // @phan-suppress-next-line PhanUndeclaredMethod Type not correctly inferred |
1073 | $permissionAction = $post->isTopicTitle() ? 'view-topic-title' : 'view'; |
1074 | if ( !$this->permissions->isAllowed( $post, $permissionAction ) ) { |
1075 | return ''; |
1076 | } |
1077 | |
1078 | // @phan-suppress-next-line PhanUndeclaredMethod Type not correctly inferred |
1079 | if ( $post->isTopicTitle() ) { |
1080 | return Message::plaintextParam( $this->templating->getContent( |
1081 | $post, 'topic-title-plaintext' ) ); |
1082 | } else { |
1083 | return Message::rawParam( $this->templating->getContent( $post, 'fixed-html' ) ); |
1084 | } |
1085 | |
1086 | case 'bundle-count': |
1087 | return Message::numParam( count( $revision ) ); |
1088 | |
1089 | default: |
1090 | wfWarn( __METHOD__ . ': Unknown formatter parameter: ' . $param ); |
1091 | return ''; |
1092 | } |
1093 | } |
1094 | |
1095 | protected function msg( $key, ...$params ) { |
1096 | if ( $params ) { |
1097 | return wfMessage( $key, ...$params ); |
1098 | } |
1099 | if ( !isset( $this->messages[$key] ) ) { |
1100 | $this->messages[$key] = new Message( $key ); |
1101 | } |
1102 | return $this->messages[$key]; |
1103 | } |
1104 | |
1105 | /** |
1106 | * Determines the exact output content format, given the requested content format |
1107 | * and the revision type. |
1108 | * |
1109 | * @param AbstractRevision $revision |
1110 | * @return string Content format |
1111 | * @throws FlowException If a per-revision format was given and it is |
1112 | * invalid for the revision type (topic title/non-topic title). |
1113 | */ |
1114 | public function decideContentFormat( AbstractRevision $revision ) { |
1115 | $requestedRevFormat = null; |
1116 | $requestedDefaultFormat = null; |
1117 | |
1118 | $alpha = $revision->getRevisionId()->getAlphadecimal(); |
1119 | if ( isset( $this->revisionContentFormat[$alpha] ) ) { |
1120 | $requestedRevFormat = $this->revisionContentFormat[$alpha]; |
1121 | } else { |
1122 | $requestedDefaultFormat = $this->contentFormat; |
1123 | } |
1124 | |
1125 | if ( $revision instanceof PostRevision && $revision->isTopicTitle() ) { |
1126 | return $this->decideTopicTitleContentFormat( |
1127 | $revision, $requestedRevFormat, $requestedDefaultFormat ); |
1128 | } else { |
1129 | return $this->decideNonTopicTitleContentFormat( |
1130 | $revision, $requestedRevFormat, $requestedDefaultFormat ); |
1131 | } |
1132 | } |
1133 | |
1134 | /** |
1135 | * Decide the content format for a topic title |
1136 | * |
1137 | * @param PostRevision $topicTitle Topic title revision |
1138 | * @param string|null $requestedRevFormat Format requested for this specific revision |
1139 | * @param string|null $requestedDefaultFormat Default format requested |
1140 | * @return string |
1141 | * @throws FlowException If a per-revision format was given and it is |
1142 | * invalid for topic titles. |
1143 | */ |
1144 | protected function decideTopicTitleContentFormat( |
1145 | PostRevision $topicTitle, |
1146 | $requestedRevFormat, |
1147 | $requestedDefaultFormat |
1148 | ) { |
1149 | if ( $requestedRevFormat !== null ) { |
1150 | if ( $requestedRevFormat !== 'topic-title-html' && |
1151 | $requestedRevFormat !== 'topic-title-wikitext' |
1152 | ) { |
1153 | throw new FlowException( 'Per-revision format for a topic title must be ' . |
1154 | '\'topic-title-html\' or \'topic-title-wikitext\'' ); |
1155 | } |
1156 | return $requestedRevFormat; |
1157 | } else { |
1158 | // Since this is a default format, we'll canonicalize it. |
1159 | |
1160 | // Because these are both editable formats, and this is the only |
1161 | // editable topic title format. |
1162 | if ( $requestedDefaultFormat === 'topic-title-wikitext' || $requestedDefaultFormat === 'html' || |
1163 | $requestedDefaultFormat === 'wikitext' |
1164 | ) { |
1165 | return 'topic-title-wikitext'; |
1166 | } else { |
1167 | return 'topic-title-html'; |
1168 | } |
1169 | } |
1170 | } |
1171 | |
1172 | /** |
1173 | * Decide the content format for revisions other than topic titles |
1174 | * |
1175 | * @param AbstractRevision $revision Revision to decide format for |
1176 | * @param string|null $requestedRevFormat Format requested for this specific revision |
1177 | * @param string|null $requestedDefaultFormat Default format requested |
1178 | * @return string |
1179 | * @throws FlowException If a per-revision format was given and it is |
1180 | * invalid for this type |
1181 | */ |
1182 | protected function decideNonTopicTitleContentFormat( |
1183 | AbstractRevision $revision, |
1184 | $requestedRevFormat, |
1185 | $requestedDefaultFormat |
1186 | ) { |
1187 | if ( $requestedRevFormat !== null ) { |
1188 | if ( $requestedRevFormat === 'topic-title-html' || |
1189 | $requestedRevFormat === 'topic-title-wikitext' |
1190 | ) { |
1191 | throw new FlowException( 'Invalid per-revision format. Only topic titles can use ' . |
1192 | '\'topic-title-html\' and \'topic-title-wikitext\'' ); |
1193 | } |
1194 | return $requestedRevFormat; |
1195 | } else { |
1196 | if ( $requestedDefaultFormat === 'topic-title-html' || |
1197 | $requestedDefaultFormat === 'topic-title-wikitext' |
1198 | ) { |
1199 | throw new FlowException( 'Default format of \'topic-title-html\' or ' . |
1200 | '\'topic-title-wikitext\' can only be used to format topic titles.' ); |
1201 | } |
1202 | |
1203 | return $requestedDefaultFormat; |
1204 | } |
1205 | } |
1206 | } |