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