Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.67% covered (warning)
70.67%
53 / 75
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiDiscussionToolsCompare
70.67% covered (warning)
70.67%
53 / 75
62.50% covered (warning)
62.50%
5 / 8
32.13
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRevision
31.25% covered (danger)
31.25%
5 / 16
0.00% covered (danger)
0.00%
0 / 1
17.70
 execute
64.29% covered (warning)
64.29%
18 / 28
0.00% covered (danger)
0.00%
0 / 1
12.69
 addResult
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isInternal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\DiscussionTools;
4
5use MediaWiki\Api\ApiBase;
6use MediaWiki\Api\ApiMain;
7use MediaWiki\Api\ApiUsageException;
8use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils;
9use MediaWiki\Extension\VisualEditor\VisualEditorParsoidClientFactory;
10use MediaWiki\Revision\RevisionLookup;
11use MediaWiki\Revision\RevisionRecord;
12use MediaWiki\Title\Title;
13use Wikimedia\ParamValidator\ParamValidator;
14
15class ApiDiscussionToolsCompare extends ApiBase {
16
17    public function __construct(
18        ApiMain $main,
19        string $name,
20        private readonly VisualEditorParsoidClientFactory $parsoidClientFactory,
21        private readonly CommentParser $commentParser,
22        private readonly RevisionLookup $revisionLookup,
23    ) {
24        parent::__construct( $main, $name );
25    }
26
27    /**
28     * @throws ApiUsageException
29     */
30    private function getRevision( array $params, string $prefix ): RevisionRecord {
31        if ( isset( $params["{$prefix}rev"] ) ) {
32            $rev = $this->revisionLookup->getRevisionById( $params["{$prefix}rev"] );
33            if ( !$rev ) {
34                $this->dieWithError( [ 'apierror-nosuchrevid', $params["{$prefix}rev"] ] );
35            }
36
37        } else {
38            $title = Title::newFromText( $params["{$prefix}title"] );
39            if ( !$title ) {
40                $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ] );
41            }
42            $rev = $this->revisionLookup->getRevisionByTitle( $title );
43            if ( !$rev ) {
44                $this->dieWithError(
45                    [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ],
46                    'nosuchrevid'
47                );
48            }
49        }
50        // To keep things simple, don't allow viewing deleted revisions through this API
51        // (even if the current user could view them if we checked with userCan()).
52        if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
53            $this->dieWithError( [ 'apierror-missingcontent-revid', $rev->getId() ], 'missingcontent' );
54        }
55        return $rev;
56    }
57
58    /**
59     * @inheritDoc
60     * @throws ApiUsageException
61     */
62    public function execute() {
63        $params = $this->extractRequestParams();
64
65        $this->requireOnlyOneParameter( $params, 'fromtitle', 'fromrev' );
66        $this->requireOnlyOneParameter( $params, 'totitle', 'torev' );
67
68        $toRev = $this->getRevision( $params, 'to' );
69
70        // When polling for new comments this is an important optimisation,
71        // as usually there is no new revision.
72        if ( $toRev->getId() === $params['fromrev'] ) {
73            $this->addResult( $toRev, $toRev );
74            return;
75        }
76
77        $fromRev = $this->getRevision( $params, 'from' );
78
79        if ( $fromRev->hasSameContent( $toRev ) ) {
80            $this->addResult( $fromRev, $toRev );
81            return;
82        }
83
84        $fromStatus = HookUtils::parseRevisionParsoidHtml( $fromRev, __METHOD__ );
85        $toStatus = HookUtils::parseRevisionParsoidHtml( $toRev, __METHOD__ );
86        if ( !$fromStatus->isOK() ) {
87            $this->dieStatus( $fromStatus );
88        }
89        if ( !$toStatus->isOK() ) {
90            $this->dieStatus( $toStatus );
91        }
92        $fromItemSet = $fromStatus->getValueOrThrow();
93        $toItemSet = $toStatus->getValueOrThrow();
94
95        $removedComments = [];
96        foreach ( $fromItemSet->getCommentItems() as $fromComment ) {
97            if ( !$toItemSet->findCommentById( $fromComment->getId() ) ) {
98                $removedComments[] = $fromComment->jsonSerializeForDiff();
99            }
100        }
101
102        $addedComments = [];
103        foreach ( $toItemSet->getCommentItems() as $toComment ) {
104            if ( !$fromItemSet->findCommentById( $toComment->getId() ) ) {
105                $addedComments[] = $toComment->jsonSerializeForDiff();
106            }
107        }
108
109        $this->addResult( $fromRev, $toRev, $removedComments, $addedComments );
110    }
111
112    /**
113     * Add the result object from revisions and comment lists
114     *
115     * @param RevisionRecord $fromRev From revision
116     * @param RevisionRecord $toRev To revision
117     * @param array $removedComments Removed comments
118     * @param array $addedComments Added comments
119     */
120    protected function addResult(
121        RevisionRecord $fromRev, RevisionRecord $toRev, array $removedComments = [], array $addedComments = []
122    ) {
123        $fromTitle = Title::newFromPageIdentity( $fromRev->getPage() );
124        $toTitle = Title::newFromPageIdentity( $toRev->getPage() );
125        $result = [
126            'fromrevid' => $fromRev->getId(),
127            'fromtitle' => $fromTitle->getPrefixedText(),
128            'torevid' => $toRev->getId(),
129            'totitle' => $toTitle->getPrefixedText(),
130            'removedcomments' => $removedComments,
131            'addedcomments' => $addedComments,
132        ];
133        $this->getResult()->addValue( null, $this->getModuleName(), $result );
134    }
135
136    /**
137     * @inheritDoc
138     */
139    public function getAllowedParams() {
140        return [
141            'fromtitle' => [
142                ApiBase::PARAM_HELP_MSG => 'apihelp-compare-param-fromtitle',
143            ],
144            'fromrev' => [
145                ParamValidator::PARAM_TYPE => 'integer',
146                ApiBase::PARAM_HELP_MSG => 'apihelp-compare-param-fromrev',
147            ],
148            'totitle' => [
149                ApiBase::PARAM_HELP_MSG => 'apihelp-compare-param-totitle',
150            ],
151            'torev' => [
152                ParamValidator::PARAM_TYPE => 'integer',
153                ApiBase::PARAM_HELP_MSG => 'apihelp-compare-param-torev',
154            ],
155        ];
156    }
157
158    /**
159     * @inheritDoc
160     */
161    public function needsToken() {
162        return false;
163    }
164
165    /**
166     * @inheritDoc
167     */
168    public function isInternal() {
169        return true;
170    }
171
172    /**
173     * @inheritDoc
174     */
175    public function isWriteMode() {
176        return false;
177    }
178}