Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.78% |
4 / 511 |
|
0.00% |
0 / 26 |
CRAP | |
0.00% |
0 / 1 |
| TopicBlock | |
0.78% |
4 / 511 |
|
0.00% |
0 / 26 |
29066.69 | |
0.00% |
0 / 1 |
| __construct | |
40.00% |
4 / 10 |
|
0.00% |
0 / 1 |
4.94 | |||
| validate | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
156 | |||
| validateEditTitle | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
132 | |||
| validateReply | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
| validateModerateTopic | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| validateModeratePost | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
| doModerate | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
132 | |||
| validateEditPost | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
90 | |||
| commit | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
462 | |||
| renderApi | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
702 | |||
| finalizeApiOutput | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
| renderDiffViewApi | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
| renderSingleViewApi | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
| renderTopicApi | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
| renderPostApi | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
42 | |||
| renderUndoApi | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
| getRevisionFormatter | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| renderTopicHistoryApi | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| renderPostHistoryApi | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| processHistoryResult | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
30 | |||
| loadRootPost | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
| loadTopicTitle | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
| getDisallowedErrorMessage | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
56 | |||
| loadRequestedPost | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
110 | |||
| getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| setPageTitle | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace Flow\Block; |
| 4 | |
| 5 | use Flow\Container; |
| 6 | use Flow\Conversion\Utils; |
| 7 | use Flow\Data\ManagerGroup; |
| 8 | use Flow\Data\Pager\HistoryPager; |
| 9 | use Flow\Exception\DataModelException; |
| 10 | use Flow\Exception\FailCommitException; |
| 11 | use Flow\Exception\FlowException; |
| 12 | use Flow\Exception\InvalidActionException; |
| 13 | use Flow\Exception\InvalidDataException; |
| 14 | use Flow\Exception\InvalidInputException; |
| 15 | use Flow\Exception\PermissionException; |
| 16 | use Flow\Formatter\PostHistoryQuery; |
| 17 | use Flow\Formatter\RevisionFormatter; |
| 18 | use Flow\Formatter\RevisionViewQuery; |
| 19 | use Flow\Formatter\TopicHistoryQuery; |
| 20 | use Flow\Model\AbstractRevision; |
| 21 | use Flow\Model\PostRevision; |
| 22 | use Flow\Model\UUID; |
| 23 | use Flow\Model\Workflow; |
| 24 | use Flow\Notifications\Controller; |
| 25 | use Flow\Repository\RootPostLoader; |
| 26 | use MediaWiki\Context\RequestContext; |
| 27 | use MediaWiki\Language\RawMessage; |
| 28 | use MediaWiki\Logging\LogEventsList; |
| 29 | use MediaWiki\Logging\LogPage; |
| 30 | use MediaWiki\MediaWikiServices; |
| 31 | use MediaWiki\Message\Message; |
| 32 | use MediaWiki\Output\OutputPage; |
| 33 | |
| 34 | class TopicBlock extends AbstractBlock { |
| 35 | |
| 36 | /** |
| 37 | * @var PostRevision|null |
| 38 | */ |
| 39 | protected $root; |
| 40 | |
| 41 | /** |
| 42 | * @var PostRevision|null |
| 43 | */ |
| 44 | protected $topicTitle; |
| 45 | |
| 46 | /** |
| 47 | * @var RootPostLoader|null |
| 48 | */ |
| 49 | protected $rootLoader; |
| 50 | |
| 51 | /** |
| 52 | * @var PostRevision|null |
| 53 | */ |
| 54 | protected $newRevision; |
| 55 | |
| 56 | /** |
| 57 | * @var array |
| 58 | */ |
| 59 | protected $requestedPost = []; |
| 60 | |
| 61 | /** |
| 62 | * @var array Map of data to be passed on as |
| 63 | * commit metadata for event handlers |
| 64 | */ |
| 65 | protected $extraCommitMetadata = []; |
| 66 | |
| 67 | /** @inheritDoc */ |
| 68 | protected $supportedPostActions = [ |
| 69 | // Standard editing |
| 70 | 'edit-post', 'reply', |
| 71 | // Moderation |
| 72 | 'moderate-topic', |
| 73 | 'moderate-post', |
| 74 | // lock or unlock topic |
| 75 | 'lock-topic', |
| 76 | // Other stuff |
| 77 | 'edit-title', |
| 78 | 'undo-edit-post', |
| 79 | ]; |
| 80 | |
| 81 | /** @inheritDoc */ |
| 82 | protected $supportedGetActions = [ |
| 83 | 'reply', 'view', 'history', 'edit-post', 'edit-title', 'compare-post-revisions', 'single-view', |
| 84 | 'view-topic', 'view-topic-history', 'view-post', 'view-post-history', 'undo-edit-post', |
| 85 | 'moderate-topic', 'moderate-post', 'lock-topic', |
| 86 | ]; |
| 87 | |
| 88 | /** |
| 89 | * @var string[] |
| 90 | * @todo Fill in the template names |
| 91 | */ |
| 92 | protected $templates = [ |
| 93 | 'single-view' => 'single_view', |
| 94 | 'view' => '', |
| 95 | 'reply' => '', |
| 96 | 'history' => 'history', |
| 97 | 'edit-post' => '', |
| 98 | 'undo-edit-post' => 'undo_edit', |
| 99 | 'edit-title' => 'edit_title', |
| 100 | 'compare-post-revisions' => 'diff_view', |
| 101 | 'moderate-topic' => 'moderate_topic', |
| 102 | 'moderate-post' => 'moderate_post', |
| 103 | 'lock-topic' => 'lock', |
| 104 | ]; |
| 105 | |
| 106 | public function __construct( Workflow $workflow, ManagerGroup $storage, $root ) { |
| 107 | parent::__construct( $workflow, $storage ); |
| 108 | if ( $root instanceof PostRevision ) { |
| 109 | $this->root = $root; |
| 110 | } elseif ( $root instanceof RootPostLoader ) { |
| 111 | $this->rootLoader = $root; |
| 112 | } else { |
| 113 | throw new DataModelException( |
| 114 | 'Expected PostRevision or RootPostLoader, received: ' . |
| 115 | get_debug_type( $root ), |
| 116 | 'invalid-input' |
| 117 | ); |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | protected function validate() { |
| 122 | $topicTitle = $this->loadTopicTitle(); |
| 123 | if ( !$topicTitle ) { |
| 124 | // permissions issue, self::loadTopicTitle should have added appropriate |
| 125 | // error messages already. |
| 126 | return; |
| 127 | } |
| 128 | |
| 129 | switch ( $this->action ) { |
| 130 | case 'edit-title': |
| 131 | $this->validateEditTitle(); |
| 132 | break; |
| 133 | |
| 134 | case 'reply': |
| 135 | $this->validateReply(); |
| 136 | break; |
| 137 | |
| 138 | case 'moderate-topic': |
| 139 | case 'lock-topic': |
| 140 | $this->validateModerateTopic(); |
| 141 | break; |
| 142 | |
| 143 | case 'moderate-post': |
| 144 | $this->validateModeratePost(); |
| 145 | break; |
| 146 | |
| 147 | case 'restore-post': |
| 148 | // @todo still necessary? |
| 149 | $this->validateModeratePost(); |
| 150 | break; |
| 151 | |
| 152 | case 'undo-edit-post': |
| 153 | case 'edit-post': |
| 154 | $this->validateEditPost(); |
| 155 | break; |
| 156 | |
| 157 | case 'edit-topic-summary': |
| 158 | // pseudo-action does not do anything, only includes data in api response |
| 159 | break; |
| 160 | |
| 161 | default: |
| 162 | throw new InvalidActionException( "Unexpected action: {$this->action}", 'invalid-action' ); |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | protected function validateEditTitle() { |
| 167 | if ( $this->workflow->isNew() ) { |
| 168 | $this->addError( 'content', $this->context->msg( 'flow-error-no-existing-workflow' ) ); |
| 169 | return; |
| 170 | } |
| 171 | if ( !isset( $this->submitted['content'] ) || !is_string( $this->submitted['content'] ) ) { |
| 172 | $this->addError( 'content', $this->context->msg( 'flow-error-missing-title' ) ); |
| 173 | return; |
| 174 | } |
| 175 | $this->submitted['content'] = trim( $this->submitted['content'] ); |
| 176 | $len = mb_strlen( $this->submitted['content'] ); |
| 177 | if ( $len === 0 ) { |
| 178 | $this->addError( 'content', $this->context->msg( 'flow-error-missing-title' ) ); |
| 179 | return; |
| 180 | } |
| 181 | if ( $len > PostRevision::MAX_TOPIC_LENGTH ) { |
| 182 | $this->addError( 'content', $this->context->msg( |
| 183 | 'flow-error-title-too-long', PostRevision::MAX_TOPIC_LENGTH ) ); |
| 184 | return; |
| 185 | } |
| 186 | if ( empty( $this->submitted['prev_revision'] ) ) { |
| 187 | $this->addError( 'prev_revision', $this->context->msg( |
| 188 | 'flow-error-missing-prev-revision-identifier' ) ); |
| 189 | return; |
| 190 | } |
| 191 | $topicTitle = $this->loadTopicTitle(); |
| 192 | if ( !$topicTitle ) { |
| 193 | return; |
| 194 | } |
| 195 | if ( !$this->permissions->isAllowed( $topicTitle, 'edit-title' ) ) { |
| 196 | $this->addError( 'permissions', $this->getDisallowedErrorMessage( $topicTitle ) ); |
| 197 | return; |
| 198 | } |
| 199 | if ( $topicTitle->getRevisionId()->getAlphadecimal() !== $this->submitted['prev_revision'] ) { |
| 200 | // This is a reasonably effective way to ensure prev revision matches, but for guarantees |
| 201 | // against race conditions there also exists a unique index on rev_prev_revision in mysql, |
| 202 | // meaning if someone else inserts against the parent we and the submitter think is the |
| 203 | // latest, our insert will fail. |
| 204 | // TODO: Catch whatever exception happens there, make sure the most recent revision is the |
| 205 | // one in the cache before handing user back to specific dialog indicating race condition |
| 206 | $this->addError( |
| 207 | 'prev_revision', |
| 208 | $this->context->msg( 'flow-error-prev-revision-mismatch' )->params( |
| 209 | $this->submitted['prev_revision'], |
| 210 | $topicTitle->getRevisionId()->getAlphadecimal(), |
| 211 | $this->context->getUser()->getName() |
| 212 | ), |
| 213 | [ 'revision_id' => $topicTitle->getRevisionId()->getAlphadecimal() ] // save current revision ID |
| 214 | ); |
| 215 | return; |
| 216 | } |
| 217 | |
| 218 | $this->newRevision = $topicTitle->newNextRevision( |
| 219 | $this->context->getUser(), |
| 220 | $this->submitted['content'], |
| 221 | 'topic-title-wikitext', |
| 222 | 'edit-title', |
| 223 | $this->workflow->getArticleTitle() |
| 224 | ); |
| 225 | if ( !$this->checkSpamFilters( $topicTitle, $this->newRevision ) ) { |
| 226 | return; |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | protected function validateReply() { |
| 231 | if ( !isset( $this->submitted['content'] ) || trim( $this->submitted['content'] ) === '' ) { |
| 232 | $this->addError( 'content', $this->context->msg( 'flow-error-missing-content' ) ); |
| 233 | return; |
| 234 | } |
| 235 | if ( !isset( $this->submitted['replyTo'] ) ) { |
| 236 | $this->addError( 'replyTo', $this->context->msg( 'flow-error-missing-replyto' ) ); |
| 237 | return; |
| 238 | } |
| 239 | |
| 240 | $post = $this->loadRequestedPost( $this->submitted['replyTo'] ); |
| 241 | if ( !$post ) { |
| 242 | return; // loadRequestedPost adds its own errors |
| 243 | } |
| 244 | if ( !$this->permissions->isAllowed( $post, 'reply' ) ) { |
| 245 | $this->addError( 'permissions', $this->getDisallowedErrorMessage( $post ) ); |
| 246 | return; |
| 247 | } |
| 248 | $this->newRevision = $post->reply( |
| 249 | $this->workflow, |
| 250 | $this->context->getUser(), |
| 251 | $this->submitted['content'], |
| 252 | // default to wikitext when not specified, for old API requests |
| 253 | $this->submitted['format'] ?? 'wikitext' |
| 254 | ); |
| 255 | if ( !$this->checkSpamFilters( null, $this->newRevision ) ) { |
| 256 | return; |
| 257 | } |
| 258 | |
| 259 | $this->extraCommitMetadata['reply-to'] = $post; |
| 260 | } |
| 261 | |
| 262 | protected function validateModerateTopic() { |
| 263 | $root = $this->loadRootPost(); |
| 264 | if ( !$root ) { |
| 265 | return; |
| 266 | } |
| 267 | |
| 268 | $this->doModerate( $root ); |
| 269 | } |
| 270 | |
| 271 | protected function validateModeratePost() { |
| 272 | if ( empty( $this->submitted['postId'] ) ) { |
| 273 | $this->addError( 'post', $this->context->msg( 'flow-error-missing-postId' ) ); |
| 274 | return; |
| 275 | } |
| 276 | |
| 277 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 278 | $post = $this->loadRequestedPost( $this->submitted['postId'] ); |
| 279 | if ( !$post ) { |
| 280 | // loadRequestedPost added its own messages to $this->errors; |
| 281 | return; |
| 282 | } |
| 283 | if ( $post->isTopicTitle() ) { |
| 284 | $this->addError( 'moderate', $this->context->msg( 'flow-error-not-a-post' ) ); |
| 285 | return; |
| 286 | } |
| 287 | $this->doModerate( $post ); |
| 288 | } |
| 289 | |
| 290 | protected function doModerate( PostRevision $post ) { |
| 291 | if ( |
| 292 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 293 | $this->submitted['moderationState'] === AbstractRevision::MODERATED_LOCKED |
| 294 | && $post->isModerated() |
| 295 | ) { |
| 296 | $this->addError( 'moderate', $this->context->msg( 'flow-error-lock-moderated-post' ) ); |
| 297 | return; |
| 298 | } |
| 299 | |
| 300 | // Moderation state supplied in request parameters |
| 301 | $moderationState = $this->submitted['moderationState'] ?? null; |
| 302 | |
| 303 | // $moderationState should be a string like 'restore', 'suppress', etc. The exact strings allowed |
| 304 | // are checked below with $post->isValidModerationState(), but this is checked first otherwise |
| 305 | // a blank string would restore a post(due to AbstractRevision::MODERATED_NONE === ''). |
| 306 | if ( !$moderationState ) { |
| 307 | $this->addError( 'moderate', $this->context->msg( 'flow-error-invalid-moderation-state' ) ); |
| 308 | return; |
| 309 | } |
| 310 | |
| 311 | /* |
| 312 | * BC: 'suppress' used to be called 'censor', 'lock' was 'close' & |
| 313 | * 'unlock' was 'reopen' |
| 314 | */ |
| 315 | $bc = [ |
| 316 | 'censor' => AbstractRevision::MODERATED_SUPPRESSED, |
| 317 | 'close' => AbstractRevision::MODERATED_LOCKED, |
| 318 | 'reopen' => 'un' . AbstractRevision::MODERATED_LOCKED |
| 319 | ]; |
| 320 | $moderationState = str_replace( array_keys( $bc ), array_values( $bc ), $moderationState ); |
| 321 | |
| 322 | // these all just mean set to no moderation, it returns a post to unmoderated status |
| 323 | $allowedRestoreAliases = [ 'unlock', 'unhide', 'undelete', 'unsuppress', /* BC for unlock: */ 'reopen' ]; |
| 324 | if ( in_array( $moderationState, $allowedRestoreAliases ) ) { |
| 325 | $moderationState = 'restore'; |
| 326 | } |
| 327 | // By allowing the moderationState to be sourced from $this->submitted['moderationState'] |
| 328 | // we no longer have a unique action name for use with the permissions system. This rebuilds |
| 329 | // an action name. e.x. restore-post, restore-topic, suppress-topic, etc. |
| 330 | $action = $moderationState . ( $post->isTopicTitle() ? "-topic" : "-post" ); |
| 331 | |
| 332 | if ( $moderationState === 'restore' ) { |
| 333 | $newState = AbstractRevision::MODERATED_NONE; |
| 334 | } else { |
| 335 | $newState = $moderationState; |
| 336 | } |
| 337 | |
| 338 | if ( !$post->isValidModerationState( $newState ) ) { |
| 339 | $this->addError( 'moderate', $this->context->msg( 'flow-error-invalid-moderation-state' ) ); |
| 340 | return; |
| 341 | } |
| 342 | if ( !$this->permissions->isAllowed( $post, $action ) ) { |
| 343 | $this->addError( 'permissions', $this->getDisallowedErrorMessage( $post ) ); |
| 344 | return; |
| 345 | } |
| 346 | |
| 347 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 348 | if ( trim( $this->submitted['reason'] ) === '' ) { |
| 349 | $this->addError( 'moderate', $this->context->msg( 'flow-error-invalid-moderation-reason' ) ); |
| 350 | return; |
| 351 | } |
| 352 | |
| 353 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 354 | $reason = $this->submitted['reason']; |
| 355 | |
| 356 | $this->newRevision = $post->moderate( $this->context->getUser(), $newState, $action, $reason ); |
| 357 | if ( !$this->newRevision ) { |
| 358 | $this->addError( 'moderate', $this->context->msg( 'flow-error-not-allowed' ) ); |
| 359 | return; |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | protected function validateEditPost() { |
| 364 | if ( empty( $this->submitted['postId'] ) ) { |
| 365 | $this->addError( 'post', $this->context->msg( 'flow-error-missing-postId' ) ); |
| 366 | return; |
| 367 | } |
| 368 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 369 | if ( trim( $this->submitted['content'] ) === '' ) { |
| 370 | $this->addError( 'content', $this->context->msg( 'flow-error-missing-content' ) ); |
| 371 | return; |
| 372 | } |
| 373 | if ( empty( $this->submitted['prev_revision'] ) ) { |
| 374 | $this->addError( 'prev_revision', $this->context->msg( 'flow-error-missing-prev-revision-identifier' ) ); |
| 375 | return; |
| 376 | } |
| 377 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 378 | $post = $this->loadRequestedPost( $this->submitted['postId'] ); |
| 379 | if ( !$post ) { |
| 380 | return; |
| 381 | } |
| 382 | if ( !$this->permissions->isAllowed( $post, 'edit-post' ) ) { |
| 383 | $this->addError( 'permissions', $this->getDisallowedErrorMessage( $post ) ); |
| 384 | return; |
| 385 | } |
| 386 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 387 | if ( $post->getRevisionId()->getAlphadecimal() !== $this->submitted['prev_revision'] ) { |
| 388 | // This is a reasonably effective way to ensure prev revision |
| 389 | // matches, but for guarantees against race conditions there |
| 390 | // also exists a unique index on rev_prev_revision in mysql, |
| 391 | // meaning if someone else inserts against the parent we and |
| 392 | // the submitter think is the latest, our insert will fail. |
| 393 | // TODO: Catch whatever exception happens there, make sure the |
| 394 | // most recent revision is the one in the cache before handing |
| 395 | // user back to specific dialog indicating race condition |
| 396 | $this->addError( |
| 397 | 'prev_revision', |
| 398 | $this->context->msg( 'flow-error-prev-revision-mismatch' )->params( |
| 399 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 400 | $this->submitted['prev_revision'], |
| 401 | $post->getRevisionId()->getAlphadecimal(), |
| 402 | $this->context->getUser()->getName() |
| 403 | ), |
| 404 | [ 'revision_id' => $post->getRevisionId()->getAlphadecimal() ] // save current revision ID |
| 405 | ); |
| 406 | return; |
| 407 | } |
| 408 | |
| 409 | $this->newRevision = $post->newNextRevision( |
| 410 | $this->context->getUser(), |
| 411 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
| 412 | $this->submitted['content'], |
| 413 | // default to wikitext when not specified, for old API requests |
| 414 | $this->submitted['format'] ?? 'wikitext', |
| 415 | 'edit-post', |
| 416 | $this->workflow->getArticleTitle() |
| 417 | ); |
| 418 | |
| 419 | if ( $this->newRevision->getRevisionId()->equals( $post->getRevisionId() ) ) { |
| 420 | $this->extraCommitMetadata['null-edit'] = true; |
| 421 | } elseif ( !$this->checkSpamFilters( $post, $this->newRevision ) ) { |
| 422 | return; |
| 423 | } |
| 424 | } |
| 425 | |
| 426 | public function commit() { |
| 427 | switch ( $this->action ) { |
| 428 | case 'edit-topic-summary': |
| 429 | // pseudo-action does not do anything, only includes data in api response |
| 430 | return []; |
| 431 | |
| 432 | case 'reply': |
| 433 | case 'moderate-topic': |
| 434 | case 'lock-topic': |
| 435 | case 'restore-post': |
| 436 | case 'moderate-post': |
| 437 | case 'edit-title': |
| 438 | case 'undo-edit-post': |
| 439 | case 'edit-post': |
| 440 | if ( $this->newRevision === null ) { |
| 441 | throw new FailCommitException( 'Attempt to save null revision', 'fail-commit' ); |
| 442 | } |
| 443 | |
| 444 | $metadata = $this->extraCommitMetadata + [ |
| 445 | 'workflow' => $this->workflow, |
| 446 | 'topic-title' => $this->loadTopicTitle(), |
| 447 | ]; |
| 448 | if ( !$metadata['topic-title'] instanceof PostRevision ) { |
| 449 | // permissions failure, should never have gotten this far |
| 450 | throw new PermissionException( 'Not Allowed', 'insufficient-permission' ); |
| 451 | } |
| 452 | if ( $this->newRevision->getPostId()->equals( $metadata['topic-title']->getPostId() ) ) { |
| 453 | // When performing actions against the topic-title self::loadTopicTitle |
| 454 | // returns the previous revision. |
| 455 | $metadata['topic-title'] = $this->newRevision; |
| 456 | } |
| 457 | |
| 458 | // store data, unless we're dealing with a null-edit (in which case |
| 459 | // is storing the same thing not only pointless, it can even be |
| 460 | // incorrect, since listeners will run & generate notifications etc) |
| 461 | if ( !isset( $this->extraCommitMetadata['null-edit'] ) ) { |
| 462 | $this->storage->put( $this->newRevision, $metadata ); |
| 463 | $this->workflow->updateLastUpdated( $this->newRevision->getRevisionId() ); |
| 464 | $this->storage->put( $this->workflow, $metadata ); |
| 465 | |
| 466 | if ( str_starts_with( $this->action, 'moderate-' ) ) { |
| 467 | $topicId = $this->newRevision->getCollection()->getRoot()->getId(); |
| 468 | |
| 469 | $moderate = $this->newRevision->isModerated() |
| 470 | && ( $this->newRevision->getModerationState() === PostRevision::MODERATED_DELETED |
| 471 | || $this->newRevision->getModerationState() === PostRevision::MODERATED_SUPPRESSED ); |
| 472 | |
| 473 | /** @var Controller $controller */ |
| 474 | $controller = Container::get( 'controller.notification' ); |
| 475 | if ( $this->action === 'moderate-topic' ) { |
| 476 | $controller->moderateTopicNotifications( $topicId, $moderate ); |
| 477 | } elseif ( $this->action === 'moderate-post' ) { |
| 478 | $postId = $this->newRevision->getPostId(); |
| 479 | $controller->moderatePostNotifications( $topicId, $postId, $moderate ); |
| 480 | } |
| 481 | } |
| 482 | } |
| 483 | |
| 484 | $newRevision = $this->newRevision; |
| 485 | |
| 486 | // If no context was loaded render the post in isolation |
| 487 | // @todo make more explicit |
| 488 | try { |
| 489 | $newRevision->getChildren(); |
| 490 | } catch ( DataModelException ) { |
| 491 | $newRevision->setChildren( [] ); |
| 492 | } |
| 493 | |
| 494 | $returnMetadata = [ |
| 495 | 'post-id' => $this->newRevision->getPostId(), |
| 496 | 'post-revision-id' => $this->newRevision->getRevisionId(), |
| 497 | ]; |
| 498 | |
| 499 | return $returnMetadata; |
| 500 | |
| 501 | default: |
| 502 | throw new InvalidActionException( "Unknown commit action: {$this->action}", 'invalid-action' ); |
| 503 | } |
| 504 | } |
| 505 | |
| 506 | public function renderApi( array $options ) { |
| 507 | $output = [ 'type' => $this->getName() ]; |
| 508 | |
| 509 | $topic = $this->loadTopicTitle(); |
| 510 | if ( !$topic ) { |
| 511 | return $output + $this->finalizeApiOutput( $options ); |
| 512 | } |
| 513 | |
| 514 | // there's probably some OO way to turn this stack of if/else into |
| 515 | // something nicer. Consider better ways before extending this with |
| 516 | // more conditionals |
| 517 | switch ( $this->action ) { |
| 518 | case 'history': |
| 519 | // single post history or full topic? |
| 520 | if ( isset( $options['postId'] ) ) { |
| 521 | // singular post history |
| 522 | $output += $this->renderPostHistoryApi( $options, UUID::create( $options['postId'] ) ); |
| 523 | } else { |
| 524 | // post history for full topic |
| 525 | $output += $this->renderTopicHistoryApi( $options ); |
| 526 | } |
| 527 | break; |
| 528 | |
| 529 | case 'single-view': |
| 530 | if ( isset( $options['revId'] ) ) { |
| 531 | $revId = $options['revId']; |
| 532 | } else { |
| 533 | throw new InvalidInputException( 'A revision must be provided', 'invalid-input' ); |
| 534 | } |
| 535 | $output += $this->renderSingleViewApi( $revId ); |
| 536 | break; |
| 537 | |
| 538 | case 'lock-topic': |
| 539 | // Treat topic as a post, only the post + summary are needed |
| 540 | $result = $this->renderPostApi( $options, $this->workflow->getId() ); |
| 541 | if ( $result !== null ) { |
| 542 | $topicId = $result['roots'][0]; |
| 543 | $revisionId = $result['posts'][$topicId][0]; |
| 544 | $output += $result['revisions'][$revisionId]; |
| 545 | } |
| 546 | break; |
| 547 | |
| 548 | case 'compare-post-revisions': |
| 549 | $output += $this->renderDiffViewApi( $options ); |
| 550 | break; |
| 551 | |
| 552 | case 'undo-edit-post': |
| 553 | $output += $this->renderUndoApi( $options ); |
| 554 | break; |
| 555 | |
| 556 | case 'view-post-history': |
| 557 | // View entire history of single post |
| 558 | $output += $this->renderPostHistoryApi( $options, UUID::create( $options['postId'] ), false ); |
| 559 | break; |
| 560 | |
| 561 | case 'view-topic-history': |
| 562 | // View entire history of a topic's posts |
| 563 | $output += $this->renderTopicHistoryApi( $options, false ); |
| 564 | break; |
| 565 | |
| 566 | // Any actions require (re)rendering the whole topic |
| 567 | case 'edit-post': |
| 568 | case 'moderate-post': |
| 569 | case 'restore-post': |
| 570 | case 'reply': |
| 571 | case 'moderate-topic': |
| 572 | case 'view-topic': |
| 573 | case 'view' && !isset( $options['postId'] ) && !isset( $options['revId'] ): |
| 574 | // view full topic |
| 575 | $output += $this->renderTopicApi( $options ); |
| 576 | break; |
| 577 | |
| 578 | case 'edit-title': |
| 579 | case 'view-post': |
| 580 | case 'view': |
| 581 | default: |
| 582 | // view single post, possibly specific revision |
| 583 | $result = $this->renderPostApi( $options ); |
| 584 | if ( $result !== null ) { |
| 585 | $output += $result; |
| 586 | } |
| 587 | break; |
| 588 | } |
| 589 | |
| 590 | return $output + $this->finalizeApiOutput( $options ); |
| 591 | } |
| 592 | |
| 593 | /** |
| 594 | * @param array $options |
| 595 | * @return array |
| 596 | */ |
| 597 | protected function finalizeApiOutput( $options ) { |
| 598 | if ( $this->wasSubmitted() ) { |
| 599 | // Failed actions, like reply, end up here |
| 600 | return [ |
| 601 | 'submitted' => $this->submitted, |
| 602 | 'errors' => $this->errors, |
| 603 | ]; |
| 604 | } else { |
| 605 | return [ |
| 606 | 'submitted' => $options, |
| 607 | 'errors' => $this->errors, |
| 608 | ]; |
| 609 | } |
| 610 | } |
| 611 | |
| 612 | /** |
| 613 | * @todo Duplicated logic in other diff view block |
| 614 | * @param array $options |
| 615 | * @return array |
| 616 | */ |
| 617 | protected function renderDiffViewApi( array $options ) { |
| 618 | if ( !isset( $options['newRevision'] ) ) { |
| 619 | throw new InvalidInputException( 'A revision must be provided for comparison', |
| 620 | 'revision-comparison' ); |
| 621 | } |
| 622 | $oldRevision = null; |
| 623 | if ( isset( $options['oldRevision'] ) ) { |
| 624 | $oldRevision = $options['oldRevision']; |
| 625 | } |
| 626 | [ $new, $old ] = Container::get( 'query.post.view' ) |
| 627 | ->getDiffViewResult( UUID::create( $options['newRevision'] ), UUID::create( $oldRevision ) ); |
| 628 | |
| 629 | return [ |
| 630 | 'revision' => Container::get( 'formatter.revision.diff.view' ) |
| 631 | ->formatApi( $new, $old, $this->context ) |
| 632 | ]; |
| 633 | } |
| 634 | |
| 635 | /** |
| 636 | * @todo Duplicated logic in other single view block |
| 637 | * @param int $revId |
| 638 | * @return array |
| 639 | */ |
| 640 | protected function renderSingleViewApi( $revId ) { |
| 641 | $row = Container::get( 'query.post.view' )->getSingleViewResult( $revId ); |
| 642 | |
| 643 | if ( !$this->permissions->isAllowed( $row->revision, 'view' ) ) { |
| 644 | $this->addError( 'permissions', $this->getDisallowedErrorMessage( $row->revision ) ); |
| 645 | return []; |
| 646 | } |
| 647 | |
| 648 | return [ |
| 649 | 'revision' => Container::get( 'formatter.revisionview' )->formatApi( $row, $this->context ) |
| 650 | ]; |
| 651 | } |
| 652 | |
| 653 | protected function renderTopicApi( array $options, $workflowId = '' ) { |
| 654 | $serializer = Container::get( 'formatter.topic' ); |
| 655 | $format = $options['format'] ?? 'fixed-html'; |
| 656 | $serializer->setContentFormat( $format ); |
| 657 | |
| 658 | if ( !$workflowId ) { |
| 659 | if ( $this->workflow->isNew() ) { |
| 660 | return $serializer->buildEmptyResult( $this->workflow ); |
| 661 | } |
| 662 | $workflowId = $this->workflow->getId(); |
| 663 | } |
| 664 | |
| 665 | if ( $this->submitted !== null ) { |
| 666 | $options += $this->submitted; |
| 667 | } |
| 668 | |
| 669 | // In the topic level responses we only want to force a single revision |
| 670 | // to wikitext (the one we're editing), not the entire thing. |
| 671 | if ( $this->action === 'edit-post' && !empty( $options['revId'] ) ) { |
| 672 | $uuid = UUID::create( $options['revId'] ); |
| 673 | if ( $uuid ) { |
| 674 | $serializer->setContentFormat( 'wikitext', $uuid ); |
| 675 | } |
| 676 | } |
| 677 | |
| 678 | return $serializer->formatApi( |
| 679 | $this->workflow, |
| 680 | Container::get( 'query.topiclist' )->getResults( [ $workflowId ] ), |
| 681 | $this->context |
| 682 | ); |
| 683 | } |
| 684 | |
| 685 | /** |
| 686 | * @todo Any failed action performed against a single revisions ends up here. |
| 687 | * To generate forms with validation errors in the non-javascript renders we |
| 688 | * need to add something to this output, but not sure what yet |
| 689 | * @param array $options |
| 690 | * @param string $postId |
| 691 | * @return null|array[] |
| 692 | * @throws FlowException |
| 693 | */ |
| 694 | protected function renderPostApi( array $options, $postId = '' ) { |
| 695 | if ( $this->workflow->isNew() ) { |
| 696 | throw new FlowException( 'No posts can exist for non-existent topic' ); |
| 697 | } |
| 698 | |
| 699 | $format = $options['format'] ?? 'fixed-html'; |
| 700 | $serializer = $this->getRevisionFormatter( $format ); |
| 701 | |
| 702 | if ( !$postId ) { |
| 703 | if ( isset( $options['postId'] ) ) { |
| 704 | $postId = $options['postId']; |
| 705 | } elseif ( $this->newRevision ) { |
| 706 | // API results after a reply will have no $postId (ID is not yet |
| 707 | // known when the reply is submitted) so we'll grab it from the |
| 708 | // newly added revision |
| 709 | $postId = $this->newRevision->getPostId(); |
| 710 | } else { |
| 711 | throw new FlowException( 'No post id specified' ); |
| 712 | } |
| 713 | } else { |
| 714 | // $postId is only set for lock-topic, which should default to |
| 715 | // wikitext instead of html |
| 716 | $format = $options['format'] ?? 'wikitext'; |
| 717 | $serializer->setContentFormat( $format, UUID::create( $postId ) ); |
| 718 | } |
| 719 | |
| 720 | $row = Container::get( 'query.singlepost' )->getResult( UUID::create( $postId ) ); |
| 721 | $serialized = $serializer->formatApi( $row, $this->context ); |
| 722 | if ( !$serialized ) { |
| 723 | return null; |
| 724 | } |
| 725 | |
| 726 | return [ |
| 727 | 'roots' => [ $serialized['postId'] ], |
| 728 | 'posts' => [ |
| 729 | $serialized['postId'] => [ $serialized['revisionId'] ], |
| 730 | ], |
| 731 | 'revisions' => [ |
| 732 | $serialized['revisionId'] => $serialized, |
| 733 | ] |
| 734 | ]; |
| 735 | } |
| 736 | |
| 737 | protected function renderUndoApi( array $options ) { |
| 738 | if ( $this->workflow->isNew() ) { |
| 739 | throw new FlowException( 'No posts can exist for non-existent topic' ); |
| 740 | } |
| 741 | |
| 742 | if ( !isset( $options['startId'] ) || !isset( $options['endId'] ) ) { |
| 743 | throw new InvalidInputException( 'Both startId and endId must be provided' ); |
| 744 | } |
| 745 | |
| 746 | /** @var RevisionViewQuery $query */ |
| 747 | $query = Container::get( 'query.post.view' ); |
| 748 | $rows = $query->getUndoDiffResult( $options['startId'], $options['endId'] ); |
| 749 | if ( !$rows ) { |
| 750 | throw new InvalidInputException( 'Could not load revision to undo' ); |
| 751 | } |
| 752 | |
| 753 | $serializer = Container::get( 'formatter.undoedit' ); |
| 754 | return $serializer->formatApi( $rows[0], $rows[1], $rows[2], $this->context ); |
| 755 | } |
| 756 | |
| 757 | /** |
| 758 | * @param string $format Content format (html|wikitext|fixed-html|topic-title-html|topic-title-wikitext) |
| 759 | * @return RevisionFormatter |
| 760 | */ |
| 761 | protected function getRevisionFormatter( $format ) { |
| 762 | $serializer = Container::get( 'formatter.revision.factory' )->create(); |
| 763 | $serializer->setContentFormat( $format ); |
| 764 | |
| 765 | return $serializer; |
| 766 | } |
| 767 | |
| 768 | protected function renderTopicHistoryApi( array $options, $navbar = true ) { |
| 769 | if ( $this->workflow->isNew() ) { |
| 770 | throw new FlowException( 'No topic history can exist for non-existent topic' ); |
| 771 | } |
| 772 | return $this->processHistoryResult( Container::get( 'query.topic.history' ), |
| 773 | $this->workflow->getId(), $options, $navbar ); |
| 774 | } |
| 775 | |
| 776 | protected function renderPostHistoryApi( array $options, UUID $postId, $navbar = true ) { |
| 777 | if ( $this->workflow->isNew() ) { |
| 778 | throw new FlowException( 'No post history can exist for non-existent topic' ); |
| 779 | } |
| 780 | return $this->processHistoryResult( Container::get( 'query.post.history' ), |
| 781 | $postId, $options, $navbar ); |
| 782 | } |
| 783 | |
| 784 | /** |
| 785 | * Process the history result for either topic or post |
| 786 | * |
| 787 | * @param TopicHistoryQuery|PostHistoryQuery $query |
| 788 | * @param UUID $uuid |
| 789 | * @param array $options |
| 790 | * @param bool $navbar Whether to include the page navbar |
| 791 | * @return array |
| 792 | */ |
| 793 | protected function processHistoryResult( |
| 794 | /* TopicHistoryQuery|PostHistoryQuery */ $query, |
| 795 | UUID $uuid, |
| 796 | $options, |
| 797 | $navbar = true |
| 798 | ) { |
| 799 | global $wgRequest; |
| 800 | |
| 801 | $format = $options['format'] ?? 'fixed-html'; |
| 802 | $serializer = $this->getRevisionFormatter( $format ); |
| 803 | $serializer->setIncludeHistoryProperties( true ); |
| 804 | |
| 805 | [ $limit, /* $offset */ ] = $wgRequest->getLimitOffsetForUser( |
| 806 | $this->context->getUser() |
| 807 | ); |
| 808 | // don't use offset from getLimitOffset - that assumes an int, which our |
| 809 | // UUIDs are not |
| 810 | $offset = $wgRequest->getText( 'offset' ); |
| 811 | $offset = $offset ? UUID::create( $offset ) : null; |
| 812 | |
| 813 | $pager = new HistoryPager( $query, $uuid ); |
| 814 | $pager->setLimit( $limit ); |
| 815 | $pager->setOffset( $offset ); |
| 816 | $pager->doQuery(); |
| 817 | $history = $pager->getResult(); |
| 818 | |
| 819 | $revisions = []; |
| 820 | foreach ( $history as $row ) { |
| 821 | // @phan-suppress-next-line PhanTypeMismatchArgument |
| 822 | $serialized = $serializer->formatApi( $row, $this->context, 'history' ); |
| 823 | // if the user is not allowed to see this row it will return empty |
| 824 | if ( $serialized ) { |
| 825 | $revisions[] = $serialized; |
| 826 | } |
| 827 | } |
| 828 | |
| 829 | $response = [ 'revisions' => $revisions ]; |
| 830 | if ( $navbar ) { |
| 831 | $response['navbar'] = $pager->getNavigationBar(); |
| 832 | } |
| 833 | return $response; |
| 834 | } |
| 835 | |
| 836 | /** |
| 837 | * @return PostRevision|null |
| 838 | */ |
| 839 | public function loadRootPost() { |
| 840 | if ( $this->root !== null ) { |
| 841 | return $this->root; |
| 842 | } |
| 843 | |
| 844 | $rootPost = $this->rootLoader->get( $this->workflow->getId() ); |
| 845 | |
| 846 | if ( $this->permissions->isAllowed( $rootPost, 'view' ) ) { |
| 847 | // topicTitle is same as root, difference is root has children populated to full depth |
| 848 | $this->topicTitle = $rootPost; |
| 849 | $this->root = $rootPost; |
| 850 | return $rootPost; |
| 851 | } |
| 852 | |
| 853 | $this->addError( 'moderation', $this->context->msg( 'flow-error-not-allowed' ) ); |
| 854 | |
| 855 | return null; |
| 856 | } |
| 857 | |
| 858 | /** |
| 859 | * @param string $action Permissions action to require to return revision |
| 860 | * @return AbstractRevision|null |
| 861 | * @throws InvalidDataException |
| 862 | */ |
| 863 | public function loadTopicTitle( $action = 'view' ) { |
| 864 | if ( $this->workflow->isNew() ) { |
| 865 | throw new InvalidDataException( 'New workflows do not have any related content', |
| 866 | 'missing-topic-title' ); |
| 867 | } |
| 868 | |
| 869 | if ( $this->topicTitle === null ) { |
| 870 | $found = $this->storage->find( |
| 871 | 'PostRevision', |
| 872 | [ 'rev_type_id' => $this->workflow->getId() ], |
| 873 | [ 'sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1 ] |
| 874 | ); |
| 875 | if ( !$found ) { |
| 876 | throw new InvalidDataException( 'Every workflow must have an associated topic title', |
| 877 | 'missing-topic-title' ); |
| 878 | } |
| 879 | $this->topicTitle = reset( $found ); |
| 880 | |
| 881 | // this method loads only title, nothing else; otherwise, you're |
| 882 | // looking for loadRootPost |
| 883 | $this->topicTitle->setChildren( [] ); |
| 884 | $this->topicTitle->setDepth( 0 ); |
| 885 | $this->topicTitle->setRootPost( $this->topicTitle ); |
| 886 | } |
| 887 | |
| 888 | if ( !$this->permissions->isAllowed( $this->topicTitle, $action ) ) { |
| 889 | $this->addError( 'permissions', $this->getDisallowedErrorMessage( $this->topicTitle ) ); |
| 890 | return null; |
| 891 | } |
| 892 | |
| 893 | return $this->topicTitle; |
| 894 | } |
| 895 | |
| 896 | /** |
| 897 | * @todo Move this to AbstractBlock and use for summary/header/etc. |
| 898 | * @param AbstractRevision $revision |
| 899 | * @return Message |
| 900 | */ |
| 901 | protected function getDisallowedErrorMessage( AbstractRevision $revision ) { |
| 902 | if ( in_array( $this->action, [ 'moderate-topic', 'moderate-post' ] ) ) { |
| 903 | /* |
| 904 | * When failing to moderate an already moderated action (like |
| 905 | * undo), show the more general "you have insufficient |
| 906 | * permissions for this action" message, rather than the |
| 907 | * specialized "this topic is <hidden|deleted|suppressed>" msg. |
| 908 | */ |
| 909 | return $this->context->msg( 'flow-error-not-allowed' ); |
| 910 | } |
| 911 | |
| 912 | $state = $revision->getModerationState(); |
| 913 | |
| 914 | // display simple message |
| 915 | // i18n messages: |
| 916 | // flow-error-not-allowed-hide, |
| 917 | // flow-error-not-allowed-reply-to-hide-topic |
| 918 | // flow-error-not-allowed-delete |
| 919 | // flow-error-not-allowed-reply-to-delete-topic |
| 920 | // flow-error-not-allowed-suppress |
| 921 | // flow-error-not-allowed-reply-to-suppress-topic |
| 922 | if ( $revision instanceof PostRevision ) { |
| 923 | $type = $revision->isTopicTitle() ? 'topic' : 'post'; |
| 924 | } else { |
| 925 | $type = $revision->getRevisionType(); |
| 926 | } |
| 927 | |
| 928 | // Show a snippet of the relevant log entry if available. |
| 929 | if ( LogPage::isLogType( $state ) ) { |
| 930 | // check if user has sufficient permissions to see log |
| 931 | $logPage = new LogPage( $state ); |
| 932 | if ( MediaWikiServices::getInstance()->getPermissionManager() |
| 933 | ->userHasRight( $this->context->getUser(), $logPage->getRestriction() ) |
| 934 | ) { |
| 935 | // LogEventsList::showLogExtract will write to OutputPage, but we |
| 936 | // actually just want that text, to write it ourselves wherever we want, |
| 937 | // so let's create an OutputPage object to then get the content from. |
| 938 | $rc = new RequestContext(); |
| 939 | $output = $rc->getOutput(); |
| 940 | |
| 941 | // get log extract |
| 942 | $entries = LogEventsList::showLogExtract( |
| 943 | $output, |
| 944 | [ $state ], |
| 945 | $this->workflow->getArticleTitle()->getPrefixedText(), |
| 946 | '', |
| 947 | [ |
| 948 | 'lim' => 10, |
| 949 | 'showIfEmpty' => false, |
| 950 | // i18n messages: |
| 951 | // flow-error-not-allowed-hide-extract |
| 952 | // flow-error-not-allowed-reply-to-hide-topic-extract |
| 953 | // flow-error-not-allowed-delete-extract |
| 954 | // flow-error-not-allowed-reply-to-delete-topic-extract |
| 955 | // flow-error-not-allowed-suppress-extract |
| 956 | // flow-error-not-allowed-reply-to-suppress-topic-extract |
| 957 | 'msgKey' => [ |
| 958 | [ |
| 959 | "flow-error-not-allowed-{$this->action}-to-$state-$type", |
| 960 | "flow-error-not-allowed-$state-extract", |
| 961 | ], |
| 962 | ] |
| 963 | ] |
| 964 | ); |
| 965 | |
| 966 | // check if there were any log extracts |
| 967 | if ( $entries ) { |
| 968 | $message = new RawMessage( '$1' ); |
| 969 | return $message->rawParams( $output->getHTML() ); |
| 970 | } |
| 971 | } |
| 972 | } |
| 973 | |
| 974 | return $this->context->msg( [ |
| 975 | // set of keys to try in order |
| 976 | "flow-error-not-allowed-{$this->action}-to-$state-$type", |
| 977 | "flow-error-not-allowed-$state", |
| 978 | "flow-error-not-allowed" |
| 979 | ] ); |
| 980 | } |
| 981 | |
| 982 | /** |
| 983 | * Loads the post referenced by $postId. Returns null when: |
| 984 | * $postId does not belong to the workflow |
| 985 | * The user does not have view access to the topic title |
| 986 | * The user does not have view access to the referenced post |
| 987 | * All these conditions add a relevant error message to $this->errors when returning null |
| 988 | * |
| 989 | * @param UUID|string $postId The post being requested |
| 990 | * @return PostRevision|null |
| 991 | */ |
| 992 | protected function loadRequestedPost( $postId ) { |
| 993 | if ( !$postId instanceof UUID ) { |
| 994 | $postId = UUID::create( $postId ); |
| 995 | } |
| 996 | '@phan-var UUID $postId'; |
| 997 | |
| 998 | if ( $this->rootLoader === null ) { |
| 999 | // Since there is no root loader the full tree is already loaded |
| 1000 | $topicTitle = $root = $this->loadRootPost(); |
| 1001 | if ( !$topicTitle ) { |
| 1002 | return null; |
| 1003 | } |
| 1004 | $post = $root->getDescendant( $postId ); |
| 1005 | if ( $post === null ) { |
| 1006 | // The requested postId is not a member of the current workflow |
| 1007 | $this->addError( 'post', $this->context->msg( |
| 1008 | 'flow-error-invalid-postId', $postId->getAlphadecimal() ) ); |
| 1009 | return null; |
| 1010 | } |
| 1011 | } else { |
| 1012 | // Load the post and its root |
| 1013 | $found = $this->rootLoader->getWithRoot( $postId ); |
| 1014 | if ( !$found['post'] || !$found['root'] || |
| 1015 | !$found['root']->getPostId()->equals( $this->workflow->getId() ) |
| 1016 | ) { |
| 1017 | $this->addError( 'post', $this->context->msg( |
| 1018 | 'flow-error-invalid-postId', $postId->getAlphadecimal() ) ); |
| 1019 | return null; |
| 1020 | } |
| 1021 | $this->topicTitle = $topicTitle = $found['root']; |
| 1022 | $post = $found['post']; |
| 1023 | |
| 1024 | // using the path to the root post, we can know the post's depth |
| 1025 | $rootPath = $this->rootLoader->getTreeRepo()->findRootPath( $postId ); |
| 1026 | $post->setDepth( count( $rootPath ) - 1 ); |
| 1027 | $post->setRootPost( $found['root'] ); |
| 1028 | } |
| 1029 | |
| 1030 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
| 1031 | if ( $this->permissions->isAllowed( $topicTitle, 'view' ) |
| 1032 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
| 1033 | && $this->permissions->isAllowed( $post, 'view' ) ) { |
| 1034 | return $post; |
| 1035 | } |
| 1036 | |
| 1037 | $this->addError( 'moderation', $this->context->msg( 'flow-error-not-allowed' ) ); |
| 1038 | return null; |
| 1039 | } |
| 1040 | |
| 1041 | /** |
| 1042 | * The prefix used for form data$pos |
| 1043 | * @return string |
| 1044 | */ |
| 1045 | public function getName() { |
| 1046 | return 'topic'; |
| 1047 | } |
| 1048 | |
| 1049 | /** |
| 1050 | * @param OutputPage $out |
| 1051 | * |
| 1052 | * @todo Provide more informative page title for actions other than view, |
| 1053 | * e.g. "Hide post in <TITLE>", "Unlock <TITLE>", etc. |
| 1054 | */ |
| 1055 | public function setPageTitle( OutputPage $out ) { |
| 1056 | $topic = $this->loadTopicTitle( $this->action === 'history' ? 'history' : 'view' ); |
| 1057 | if ( !$topic ) { |
| 1058 | return; |
| 1059 | } |
| 1060 | |
| 1061 | $title = $this->workflow->getOwnerTitle(); |
| 1062 | $convertedTitle = Utils::getConvertedTitle( $title ); |
| 1063 | $out->setPageTitleMsg( $out->msg( 'flow-topic-first-heading', $convertedTitle ) ); |
| 1064 | if ( $this->permissions->isAllowed( $topic, 'view' ) ) { |
| 1065 | if ( $this->action === 'undo-edit-post' ) { |
| 1066 | $key = 'flow-undo-edit-post'; |
| 1067 | } else { |
| 1068 | $key = 'flow-topic-html-title'; |
| 1069 | } |
| 1070 | $out->setHTMLTitle( $out->msg( $key, |
| 1071 | // This must be a rawParam to not expand {{foo}} in the title, it must |
| 1072 | // not be htmlspecialchar'd because OutputPage::setHtmlTitle handles that. |
| 1073 | Message::rawParam( $topic->getContent( 'topic-title-plaintext' ) ), |
| 1074 | $convertedTitle |
| 1075 | ) ); |
| 1076 | } else { |
| 1077 | $out->setHTMLTitle( $convertedTitle ); |
| 1078 | } |
| 1079 | $out->setSubtitle( '< ' . |
| 1080 | MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( $title ) ); |
| 1081 | } |
| 1082 | } |