Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.67% covered (warning)
86.67%
65 / 75
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReviewTranslationActionApi
86.67% covered (warning)
86.67%
65 / 75
50.00% covered (danger)
50.00%
4 / 8
18.77
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 execute
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
5.08
 doReview
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
2
 getReviewBlockers
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
6.13
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
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
 getAllowedParams
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\TranslatorInterface;
5
6use ManualLogEntry;
7use MediaWiki\Api\ApiBase;
8use MediaWiki\Api\ApiMain;
9use MediaWiki\Extension\Translate\HookRunner;
10use MediaWiki\Extension\Translate\MessageLoading\MessageHandle;
11use MediaWiki\Revision\RevisionLookup;
12use MediaWiki\Revision\RevisionRecord;
13use MediaWiki\Status\Status;
14use MediaWiki\Title\TitleFormatter;
15use MediaWiki\User\User;
16use Wikimedia\ParamValidator\ParamValidator;
17use Wikimedia\Rdbms\ILoadBalancer;
18
19/**
20 * API module for marking translations as reviewed
21 * @author Niklas Laxström
22 * @license GPL-2.0-or-later
23 * @ingroup API TranslateAPI
24 */
25class ReviewTranslationActionApi extends ApiBase {
26    /** @var string */
27    protected static $right = 'translate-messagereview';
28    private RevisionLookup $revisionLookup;
29    private TitleFormatter $titleFormatter;
30    private ILoadBalancer $loadBalancer;
31    private HookRunner $hookRunner;
32
33    public function __construct(
34        ApiMain $main,
35        string $moduleName,
36        RevisionLookup $revisionLookup,
37        TitleFormatter $titleFormatter,
38        ILoadBalancer $loadBalancer,
39        HookRunner $hookRunner
40    ) {
41        parent::__construct( $main, $moduleName );
42        $this->revisionLookup = $revisionLookup;
43        $this->titleFormatter = $titleFormatter;
44        $this->loadBalancer = $loadBalancer;
45        $this->hookRunner = $hookRunner;
46    }
47
48    public function execute() {
49        $this->checkUserRightsAny( self::$right );
50
51        $params = $this->extractRequestParams();
52
53        $revRecord = $this->revisionLookup->getRevisionById( $params['revision'] );
54        if ( !$revRecord ) {
55            $this->dieWithError( [ 'apierror-nosuchrevid', $params['revision'] ], 'invalidrevision' );
56        }
57
58        $status = $this->getReviewBlockers( $this->getUser(), $revRecord );
59        if ( !$status->isGood() ) {
60            if ( $status->hasMessage( 'blocked' ) ) {
61                $this->dieBlocked( $this->getUser()->getBlock() );
62            } else {
63                $this->dieStatus( $status );
64            }
65        }
66
67        $ok = $this->doReview( $this->getUser(), $revRecord );
68        if ( !$ok ) {
69            $this->addWarning( 'apiwarn-translate-alreadyreviewedbyyou' );
70        }
71
72        $prefixedText = $this->titleFormatter->getPrefixedText( $revRecord->getPageAsLinkTarget() );
73        $output = [ 'review' => [
74            'title' => $prefixedText,
75            'pageid' => $revRecord->getPageId(),
76            'revision' => $revRecord->getId()
77        ] ];
78
79        $this->getResult()->addValue( null, $this->getModuleName(), $output );
80    }
81
82    /**
83     * Executes the real stuff. No checks done!
84     * @return bool whether the action was recorded.
85     */
86    private function doReview( User $user, RevisionRecord $revRecord ): bool {
87        $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
88        $dbw->newInsertQueryBuilder()
89            ->insertInto( 'translate_reviews' )
90            ->ignore()
91            ->row( [
92                'trr_user' => $user->getId(),
93                'trr_page' => $revRecord->getPageId(),
94                'trr_revision' => $revRecord->getId(),
95            ] )
96            ->caller( __METHOD__ )
97            ->execute();
98
99        if ( !$dbw->affectedRows() ) {
100            return false;
101        }
102
103        $title = $revRecord->getPageAsLinkTarget();
104
105        $entry = new ManualLogEntry( 'translationreview', 'message' );
106        $entry->setPerformer( $user );
107        $entry->setTarget( $title );
108        $entry->setParameters( [
109            '4::revision' => $revRecord->getId(),
110        ] );
111
112        $logid = $entry->insert();
113        $entry->publish( $logid );
114
115        $handle = new MessageHandle( $title );
116        $this->hookRunner->onTranslateEventTranslationReview( $handle );
117
118        return true;
119    }
120
121    /**
122     * Validates review action by checking permissions and other things.
123     * @return Status Contains error key that describes the review blocker.
124     */
125    private function getReviewBlockers( User $user, RevisionRecord $revRecord ): Status {
126        if ( !$user->isAllowed( self::$right ) ) {
127            return Status::newFatal( 'apierror-permissiondenied-generic' );
128        }
129
130        if ( $user->getBlock() ) {
131            return Status::newFatal( 'blocked' );
132        }
133
134        $title = $revRecord->getPageAsLinkTarget();
135        $handle = new MessageHandle( $title );
136        if ( !$handle->isValid() ) {
137            return Status::newFatal( 'apierror-translate-unknownmessage' );
138        }
139
140        if ( $user->equals( $revRecord->getUser() ) ) {
141            return Status::newFatal( 'apierror-translate-owntranslation' );
142        }
143
144        if ( $handle->isFuzzy() ) {
145            return Status::newFatal( 'apierror-translate-fuzzymessage' );
146        }
147
148        return Status::newGood();
149    }
150
151    public function isWriteMode(): bool {
152        return true;
153    }
154
155    public function needsToken(): string {
156        return 'csrf';
157    }
158
159    protected function getAllowedParams(): array {
160        return [
161            'revision' => [
162                ParamValidator::PARAM_TYPE => 'integer',
163                ParamValidator::PARAM_REQUIRED => true,
164            ],
165        ];
166    }
167
168    protected function getExamplesMessages(): array {
169        return [
170            'action=translationreview&revision=1&token=foo'
171                => 'apihelp-translationreview-example-1',
172        ];
173    }
174}