Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 212
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
TopicSummaryBlock
0.00% covered (danger)
0.00%
0 / 212
0.00% covered (danger)
0.00%
0 / 11
3540
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 validate
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 validateTopicSummary
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
240
 findTopicTitle
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 saveTopicSummary
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 commit
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 renderApi
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
182
 renderNewestTopicSummary
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
30
 renderUndoApi
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPageTitle
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace Flow\Block;
4
5use Flow\Container;
6use Flow\Conversion\Utils;
7use Flow\Exception\FailCommitException;
8use Flow\Exception\FlowException;
9use Flow\Exception\InvalidActionException;
10use Flow\Exception\InvalidDataException;
11use Flow\Exception\InvalidInputException;
12use Flow\Formatter\FormatterRow;
13use Flow\Formatter\PostSummaryQuery;
14use Flow\Formatter\PostSummaryViewQuery;
15use Flow\Formatter\RevisionViewFormatter;
16use Flow\Formatter\RevisionViewQuery;
17use Flow\Model\PostRevision;
18use Flow\Model\PostSummary;
19use Flow\Model\UUID;
20use IContextSource;
21use MediaWiki\MediaWikiServices;
22use MediaWiki\Output\OutputPage;
23use Message;
24
25class 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 */
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->setPageTitle( $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( '&lt; ' .
444            MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( $title ) );
445    }
446}