Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 212 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
TopicSummaryBlock | |
0.00% |
0 / 212 |
|
0.00% |
0 / 11 |
3540 | |
0.00% |
0 / 1 |
init | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
validate | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
validateTopicSummary | |
0.00% |
0 / 58 |
|
0.00% |
0 / 1 |
240 | |||
findTopicTitle | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
saveTopicSummary | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
commit | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
renderApi | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
182 | |||
renderNewestTopicSummary | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
30 | |||
renderUndoApi | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setPageTitle | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | namespace Flow\Block; |
4 | |
5 | use Flow\Container; |
6 | use Flow\Conversion\Utils; |
7 | use Flow\Exception\FailCommitException; |
8 | use Flow\Exception\FlowException; |
9 | use Flow\Exception\InvalidActionException; |
10 | use Flow\Exception\InvalidDataException; |
11 | use Flow\Exception\InvalidInputException; |
12 | use Flow\Formatter\FormatterRow; |
13 | use Flow\Formatter\PostSummaryQuery; |
14 | use Flow\Formatter\PostSummaryViewQuery; |
15 | use Flow\Formatter\RevisionViewFormatter; |
16 | use Flow\Formatter\RevisionViewQuery; |
17 | use Flow\Model\PostRevision; |
18 | use Flow\Model\PostSummary; |
19 | use Flow\Model\UUID; |
20 | use MediaWiki\Context\IContextSource; |
21 | use MediaWiki\MediaWikiServices; |
22 | use MediaWiki\Message\Message; |
23 | use MediaWiki\Output\OutputPage; |
24 | |
25 | class TopicSummaryBlock extends AbstractBlock { |
26 | /** |
27 | * @var PostSummary|null |
28 | */ |
29 | protected $topicSummary; |
30 | |
31 | /** |
32 | * @var FormatterRow |
33 | */ |
34 | protected $formatterRow; |
35 | |
36 | /** |
37 | * @var PostSummary|null |
38 | */ |
39 | protected $nextRevision; |
40 | |
41 | /** |
42 | * @var array Map of data to be passed on as |
43 | * commit metadata for event handlers |
44 | */ |
45 | protected $extraCommitMetadata = []; |
46 | |
47 | /** |
48 | * @var PostRevision|null |
49 | */ |
50 | protected $topicTitle; |
51 | |
52 | /** |
53 | * @var string[] |
54 | */ |
55 | protected $supportedPostActions = [ 'edit-topic-summary', 'undo-edit-topic-summary' ]; |
56 | |
57 | /** |
58 | * @var string[] |
59 | */ |
60 | protected $supportedGetActions = [ |
61 | 'view-topic-summary', 'compare-postsummary-revisions', 'edit-topic-summary', |
62 | 'undo-edit-topic-summary' |
63 | ]; |
64 | |
65 | /** @inheritDoc */ |
66 | protected $templates = [ |
67 | 'view-topic-summary' => 'single_view', |
68 | 'compare-postsummary-revisions' => 'diff_view', |
69 | 'edit-topic-summary' => 'edit', |
70 | 'undo-edit-topic-summary' => 'undo_edit', |
71 | ]; |
72 | |
73 | /** |
74 | * @param IContextSource $context |
75 | * @param string $action |
76 | */ |
77 | public function init( IContextSource $context, $action ) { |
78 | parent::init( $context, $action ); |
79 | |
80 | if ( !$this->workflow->isNew() ) { |
81 | /** @var PostSummaryQuery $query */ |
82 | $query = Container::get( 'query.postsummary' ); |
83 | $this->formatterRow = $query->getResult( $this->workflow->getId() ); |
84 | if ( $this->formatterRow ) { |
85 | $this->topicSummary = $this->formatterRow->revision; |
86 | } |
87 | } |
88 | } |
89 | |
90 | /** |
91 | * Validate data before commiting change |
92 | */ |
93 | public function validate() { |
94 | switch ( $this->action ) { |
95 | case 'undo-edit-topic-summary': |
96 | case 'edit-topic-summary': |
97 | $this->validateTopicSummary(); |
98 | break; |
99 | |
100 | default: |
101 | throw new InvalidActionException( "Unexpected action: {$this->action}", 'invalid-action' ); |
102 | } |
103 | } |
104 | |
105 | /** |
106 | * @throws InvalidDataException |
107 | */ |
108 | protected function validateTopicSummary() { |
109 | if ( !isset( $this->submitted['summary'] ) || !is_string( $this->submitted['summary'] ) ) { |
110 | $this->addError( 'content', $this->context->msg( 'flow-error-missing-summary' ) ); |
111 | return; |
112 | } |
113 | |
114 | if ( $this->workflow->isNew() ) { |
115 | throw new InvalidDataException( 'Topic summary can only be added to an existing topic', |
116 | 'missing-topic-title' ); |
117 | } |
118 | |
119 | // Create topic summary |
120 | if ( !$this->topicSummary ) { |
121 | $topicTitle = $this->findTopicTitle(); |
122 | $boardWorkflow = $topicTitle->getCollection()->getBoardWorkflow(); |
123 | if ( |
124 | !$this->permissions->isRevisionAllowed( null, 'create-topic-summary' ) || |
125 | !$this->permissions->isRootAllowed( $topicTitle, 'create-topic-summary' ) || |
126 | !$this->permissions->isBoardAllowed( $boardWorkflow, 'create-topic-summary' ) |
127 | ) { |
128 | $this->addError( 'permissions', $this->context->msg( 'flow-error-not-allowed' ) ); |
129 | return; |
130 | } |
131 | // new summary should not have a previous revision |
132 | if ( !empty( $this->submitted['prev_revision'] ) ) { |
133 | $this->addError( 'prev_revision', |
134 | $this->context->msg( 'flow-error-prev-revision-does-not-exist' ) ); |
135 | return; |
136 | } |
137 | |
138 | $this->nextRevision = PostSummary::create( |
139 | $this->workflow->getArticleTitle(), |
140 | $this->findTopicTitle(), |
141 | $this->context->getUser(), |
142 | $this->submitted['summary'], |
143 | // default to wikitext when not specified, for old API requests |
144 | $this->submitted['format'] ?? 'wikitext', |
145 | 'create-topic-summary' |
146 | ); |
147 | |
148 | if ( !trim( $this->submitted['summary'] ) ) { |
149 | $this->extraCommitMetadata['null-edit'] = true; |
150 | } |
151 | // Edit topic summary |
152 | } else { |
153 | if ( !$this->permissions->isAllowed( $this->topicSummary, 'edit-topic-summary' ) ) { |
154 | $this->addError( 'permissions', $this->context->msg( 'flow-error-not-allowed' ) ); |
155 | return; |
156 | } |
157 | // Check the previous revision to catch possible edit conflict |
158 | if ( empty( $this->submitted['prev_revision'] ) ) { |
159 | $this->addError( 'prev_revision', |
160 | $this->context->msg( 'flow-error-missing-prev-revision-identifier' ) ); |
161 | return; |
162 | } elseif ( $this->topicSummary->getRevisionId()->getAlphadecimal() !== |
163 | $this->submitted['prev_revision'] |
164 | ) { |
165 | $this->addError( |
166 | 'prev_revision', |
167 | $this->context->msg( 'flow-error-prev-revision-mismatch' )->params( |
168 | $this->submitted['prev_revision'], |
169 | $this->topicSummary->getRevisionId()->getAlphadecimal(), |
170 | $this->context->getUser()->getName() |
171 | ), |
172 | [ 'revision_id' => $this->topicSummary->getRevisionId()->getAlphadecimal() ] |
173 | ); |
174 | return; |
175 | } |
176 | |
177 | $this->nextRevision = $this->topicSummary->newNextRevision( |
178 | $this->context->getUser(), |
179 | $this->submitted['summary'], |
180 | // default to wikitext when not specified, for old API requests |
181 | $this->submitted['format'] ?? 'wikitext', |
182 | 'edit-topic-summary', |
183 | $this->workflow->getArticleTitle() |
184 | ); |
185 | |
186 | if ( $this->nextRevision->getRevisionId()->equals( $this->topicSummary->getRevisionId() ) ) { |
187 | $this->extraCommitMetadata['null-edit'] = true; |
188 | } |
189 | } |
190 | |
191 | if ( !$this->checkSpamFilters( $this->topicSummary, $this->nextRevision ) ) { |
192 | return; |
193 | } |
194 | } |
195 | |
196 | /** |
197 | * Find the topic title for the summary |
198 | * |
199 | * @throws InvalidDataException |
200 | * @return PostRevision |
201 | */ |
202 | public function findTopicTitle() { |
203 | if ( $this->topicTitle ) { |
204 | return $this->topicTitle; |
205 | } |
206 | $found = $this->storage->find( |
207 | 'PostRevision', |
208 | [ 'rev_type_id' => $this->workflow->getId() ], |
209 | [ 'sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1 ] |
210 | ); |
211 | if ( !$found ) { |
212 | throw new InvalidDataException( 'Every workflow must have an associated topic title', |
213 | 'missing-topic-title' ); |
214 | } |
215 | $this->topicTitle = reset( $found ); |
216 | return $this->topicTitle; |
217 | } |
218 | |
219 | /** |
220 | * @throws FailCommitException |
221 | * @return array |
222 | */ |
223 | protected function saveTopicSummary() { |
224 | if ( !$this->nextRevision ) { |
225 | throw new FailCommitException( 'Attempt to save summary on null revision', 'fail-commit' ); |
226 | } |
227 | |
228 | // store data, unless we're dealing with a null-edit (in which case |
229 | // is storing the same thing not only pointless, it can even be |
230 | // incorrect, since listeners will run & generate notifications etc) |
231 | if ( !isset( $this->extraCommitMetadata['null-edit'] ) ) { |
232 | $this->storage->put( $this->nextRevision, $this->extraCommitMetadata + [ |
233 | 'workflow' => $this->workflow, |
234 | 'topic-title' => $this->findTopicTitle(), |
235 | ] ); |
236 | } |
237 | // Reload the $this->formatterRow for renderApi() after save |
238 | $this->formatterRow = new FormatterRow(); |
239 | $this->formatterRow->revision = $this->nextRevision; |
240 | $this->formatterRow->previousRevision = $this->topicSummary; |
241 | $this->formatterRow->currentRevision = $this->nextRevision; |
242 | $this->formatterRow->workflow = $this->workflow; |
243 | $this->topicSummary = $this->nextRevision; |
244 | |
245 | return [ |
246 | 'summary-revision-id' => $this->nextRevision->getRevisionId(), |
247 | ]; |
248 | } |
249 | |
250 | /** |
251 | * Save change for any valid committed action |
252 | * |
253 | * @throws InvalidActionException |
254 | * @return array |
255 | */ |
256 | public function commit() { |
257 | switch ( $this->action ) { |
258 | case 'undo-edit-topic-summary': |
259 | case 'edit-topic-summary': |
260 | return $this->saveTopicSummary(); |
261 | |
262 | default: |
263 | throw new InvalidActionException( "Unexpected action: {$this->action}", |
264 | 'invalid-action' ); |
265 | } |
266 | } |
267 | |
268 | /** |
269 | * Render the data for API request |
270 | * |
271 | * @param array $options |
272 | * @return array |
273 | * @throws InvalidInputException |
274 | */ |
275 | public function renderApi( array $options ) { |
276 | $output = [ 'type' => $this->getName() ]; |
277 | |
278 | switch ( $this->action ) { |
279 | case 'view-topic-summary': |
280 | // @Todo - duplicated logic in other single view block |
281 | if ( isset( $options['revId'] ) && $options['revId'] ) { |
282 | /** @var PostSummaryViewQuery $query */ |
283 | $query = Container::get( 'query.postsummary.view' ); |
284 | $row = $query->getSingleViewResult( $options['revId'] ); |
285 | if ( !$this->permissions->isAllowed( $row->revision, 'view-topic-summary' ) ) { |
286 | $this->addError( 'permissions', $this->context->msg( 'flow-error-not-allowed' ) ); |
287 | break; |
288 | } |
289 | |
290 | /** @var RevisionViewFormatter $formatter */ |
291 | $formatter = Container::get( 'formatter.revisionview' ); |
292 | $output['revision'] = $formatter->formatApi( $row, $this->context ); |
293 | } else { |
294 | $format = $options['format'] ?? 'fixed-html'; |
295 | $output += $this->renderNewestTopicSummary( $format ); |
296 | } |
297 | break; |
298 | case 'edit-topic-summary': |
299 | // default to wikitext for no-JS |
300 | $format = $options['format'] ?? 'wikitext'; |
301 | $output += $this->renderNewestTopicSummary( $format ); |
302 | break; |
303 | case 'undo-edit-topic-summary': |
304 | $output = $this->renderUndoApi( $options ) + $output; |
305 | break; |
306 | case 'compare-postsummary-revisions': |
307 | // @Todo - duplicated logic in other diff view block |
308 | if ( !isset( $options['newRevision'] ) ) { |
309 | throw new InvalidInputException( 'A revision must be provided for comparison', |
310 | 'revision-comparison' ); |
311 | } |
312 | $oldRevision = null; |
313 | if ( isset( $options['oldRevision'] ) ) { |
314 | $oldRevision = $options['oldRevision']; |
315 | } |
316 | [ $new, $old ] = Container::get( 'query.postsummary.view' )->getDiffViewResult( |
317 | UUID::create( $options['newRevision'] ), |
318 | UUID::create( $oldRevision ) |
319 | ); |
320 | if ( |
321 | !$this->permissions->isAllowed( $new->revision, 'view-topic-summary' ) || |
322 | !$this->permissions->isAllowed( $old->revision, 'view-topic-summary' ) |
323 | ) { |
324 | $this->addError( 'permissions', $this->context->msg( 'flow-error-not-allowed' ) ); |
325 | break; |
326 | } |
327 | $output['revision'] = Container::get( 'formatter.revision.diff.view' ) |
328 | ->formatApi( $new, $old, $this->context ); |
329 | break; |
330 | } |
331 | |
332 | if ( $this->wasSubmitted() ) { |
333 | $output += [ |
334 | 'submitted' => $this->submitted, |
335 | 'errors' => $this->errors, |
336 | ]; |
337 | } else { |
338 | $output += [ |
339 | 'submitted' => [], |
340 | 'errors' => $this->errors, |
341 | ]; |
342 | } |
343 | |
344 | return $output; |
345 | } |
346 | |
347 | /** |
348 | * @param string $format Content format (wikitext|html) |
349 | * @return array |
350 | */ |
351 | protected function renderNewestTopicSummary( $format ) { |
352 | $topicTitle = $this->findTopicTitle(); |
353 | $boardWorkflow = $topicTitle->getCollection()->getBoardWorkflow(); |
354 | if ( |
355 | // topicSummary can be null PostSummary object or null (doesn't exist yet) |
356 | !$this->permissions->isRevisionAllowed( $this->topicSummary, 'view-topic-summary' ) || |
357 | !$this->permissions->isRootAllowed( $topicTitle, 'view-topic-summary' ) || |
358 | !$this->permissions->isBoardAllowed( $boardWorkflow, 'view-topic-summary' ) |
359 | ) { |
360 | $this->addError( 'permissions', $this->context->msg( 'flow-error-not-allowed' ) ); |
361 | return []; |
362 | } |
363 | |
364 | $output = []; |
365 | $formatter = Container::get( 'formatter.revision.factory' )->create(); |
366 | $formatter->setContentFormat( $format ); |
367 | |
368 | if ( $this->formatterRow ) { |
369 | $output['revision'] = $formatter->formatApi( |
370 | $this->formatterRow, |
371 | $this->context |
372 | ); |
373 | } else { |
374 | $urlGenerator = Container::get( 'url_generator' ); |
375 | $title = $this->workflow->getArticleTitle(); |
376 | $workflowId = $this->workflow->getId(); |
377 | $output['revision'] = [ |
378 | 'actions' => [ |
379 | 'summarize' => $urlGenerator->editTopicSummaryAction( |
380 | $title, |
381 | $workflowId |
382 | ) |
383 | ], |
384 | 'links' => [ |
385 | 'topic' => $urlGenerator->topicLink( |
386 | $title, |
387 | $workflowId |
388 | ) |
389 | ] |
390 | ]; |
391 | } |
392 | return $output; |
393 | } |
394 | |
395 | protected function renderUndoApi( array $options ) { |
396 | if ( $this->workflow->isNew() ) { |
397 | throw new FlowException( 'No topic exists to undo' ); |
398 | } |
399 | |
400 | if ( !isset( $options['startId'] ) || !isset( $options['endId'] ) ) { |
401 | throw new InvalidInputException( 'Both startId and endId must be provided' ); |
402 | } |
403 | |
404 | /** @var RevisionViewQuery $query */ |
405 | $query = Container::get( 'query.postsummary.view' ); |
406 | $rows = $query->getUndoDiffResult( $options['startId'], $options['endId'] ); |
407 | if ( !$rows ) { |
408 | throw new InvalidInputException( 'Could not load revision to undo' ); |
409 | } |
410 | |
411 | $serializer = Container::get( 'formatter.undoedit' ); |
412 | return $serializer->formatApi( $rows[0], $rows[1], $rows[2], $this->context ); |
413 | } |
414 | |
415 | public function getName() { |
416 | return 'topicsummary'; |
417 | } |
418 | |
419 | /** |
420 | * @param OutputPage $out |
421 | */ |
422 | public function setPageTitle( OutputPage $out ) { |
423 | $topic = $this->findTopicTitle(); |
424 | $title = $this->workflow->getOwnerTitle(); |
425 | $convertedTitle = Utils::getConvertedTitle( $title ); |
426 | $out->setPageTitleMsg( $out->msg( 'flow-topic-first-heading', $convertedTitle ) ); |
427 | if ( $this->permissions->isAllowed( $topic, 'view' ) ) { |
428 | if ( $this->action === 'undo-edit-topic-summary' ) { |
429 | $key = 'flow-undo-edit-topic-summary'; |
430 | } else { |
431 | $key = 'flow-topic-html-title'; |
432 | } |
433 | $out->setHTMLTitle( $out->msg( $key, |
434 | // This must be a rawParam to not expand {{foo}} in the title, it must |
435 | // not be htmlspecialchar'd because OutputPage::setHtmlTitle handles that. |
436 | Message::rawParam( $topic->getContent( 'topic-title-plaintext' ) ), |
437 | $convertedTitle |
438 | ) ); |
439 | } else { |
440 | $out->setHTMLTitle( $convertedTitle ); |
441 | } |
442 | |
443 | $out->setSubtitle( '< ' . |
444 | MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( $title ) ); |
445 | } |
446 | } |