Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 133
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
TranslatorSandboxActionApi
0.00% covered (danger)
0.00%
0 / 133
0.00% covered (danger)
0.00%
0 / 10
992
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 doCreate
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
90
 doDelete
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 doPromote
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 doRemind
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 createUserPage
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 isWriteMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 needsToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\TranslatorSandbox;
5
6use ManualLogEntry;
7use MediaWiki\Api\ApiBase;
8use MediaWiki\Api\ApiMain;
9use MediaWiki\CommentStore\CommentStoreComment;
10use MediaWiki\Config\ServiceOptions;
11use MediaWiki\Content\ContentHandler;
12use MediaWiki\Json\FormatJson;
13use MediaWiki\Page\WikiPageFactory;
14use MediaWiki\Parser\Sanitizer;
15use MediaWiki\Revision\SlotRecord;
16use MediaWiki\User\Options\UserOptionsLookup;
17use MediaWiki\User\Options\UserOptionsManager;
18use MediaWiki\User\User;
19use MediaWiki\User\UserFactory;
20use MediaWiki\User\UserNameUtils;
21use RuntimeException;
22use Wikimedia\ParamValidator\ParamValidator;
23
24/**
25 * WebAPI for the sandbox feature of Translate.
26 * @author Niklas Laxström
27 * @license GPL-2.0-or-later
28 * @ingroup API TranslateAPI
29 */
30class TranslatorSandboxActionApi extends ApiBase {
31    private UserFactory $userFactory;
32    private UserNameUtils $userNameUtils;
33    private UserOptionsManager $userOptionsManager;
34    private WikiPageFactory $wikiPageFactory;
35    private UserOptionsLookup $userOptionsLookup;
36    private TranslateSandbox $translateSandbox;
37    private bool $isSandboxEnabled;
38    public const CONSTRUCTOR_OPTIONS = [
39        'TranslateUseSandbox',
40    ];
41
42    public function __construct(
43        ApiMain $mainModule,
44        string $moduleName,
45        UserFactory $userFactory,
46        UserNameUtils $userNameUtils,
47        UserOptionsManager $userOptionsManager,
48        WikiPageFactory $wikiPageFactory,
49        UserOptionsLookup $userOptionsLookup,
50        TranslateSandbox $translateSandbox,
51        ServiceOptions $options
52    ) {
53        parent::__construct( $mainModule, $moduleName );
54        $this->userFactory = $userFactory;
55        $this->userNameUtils = $userNameUtils;
56        $this->userOptionsManager = $userOptionsManager;
57        $this->wikiPageFactory = $wikiPageFactory;
58        $this->userOptionsLookup = $userOptionsLookup;
59        $this->translateSandbox = $translateSandbox;
60        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
61        $this->isSandboxEnabled = $options->get( 'TranslateUseSandbox' );
62    }
63
64    public function execute(): void {
65        if ( !$this->isSandboxEnabled ) {
66            $this->dieWithError( 'apierror-translate-sandboxdisabled', 'sandboxdisabled' );
67        }
68
69        $params = $this->extractRequestParams();
70        switch ( $params['do'] ) {
71            case 'create':
72                $this->doCreate();
73                break;
74            case 'delete':
75                $this->doDelete();
76                break;
77            case 'promote':
78                $this->doPromote();
79                break;
80            case 'remind':
81                $this->doRemind();
82                break;
83            default:
84                $this->dieWithError( [ 'apierror-badparameter', 'do' ] );
85        }
86    }
87
88    private function doCreate(): void {
89        $params = $this->extractRequestParams();
90
91        // Do validations
92        foreach ( explode( '|', 'username|password|email' ) as $field ) {
93            if ( !isset( $params[$field] ) ) {
94                $this->dieWithError( [ 'apierror-missingparam', $field ], 'missingparam' );
95            }
96        }
97
98        $username = $params['username'];
99
100        $canonicalName = $this->userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_CREATABLE );
101
102        if ( $canonicalName === false ) {
103            $this->dieWithError( 'noname', 'invalidusername' );
104        }
105
106        $user = $this->userFactory->newFromName( $username );
107        if ( $user->getId() !== 0 ) {
108            $this->dieWithError( 'userexists', 'nonfreeusername' );
109        }
110
111        $password = $params['password'];
112        $passwordValidityStatus = $user->checkPasswordValidity( $password );
113        if ( !$passwordValidityStatus->isGood() ) {
114            $this->dieStatus( $passwordValidityStatus );
115        }
116
117        $email = $params['email'];
118        if ( !Sanitizer::validateEmail( $email ) ) {
119            $this->dieWithError( 'invalidemailaddress', 'invalidemail' );
120        }
121
122        try {
123            $user = $this->translateSandbox->addUser( $username, $email, $password );
124        } catch ( RuntimeException $e ) {
125            // Do not log this error as it might leak private information
126            if ( $e->getCode() === TranslateSandbox::USER_CREATION_FAILURE ) {
127                $this->dieWithError( 'apierror-translate-sandbox-user-add' );
128            }
129
130            throw $e;
131        }
132
133        $output = [ 'user' => [
134            'name' => $user->getName(),
135            'id' => $user->getId(),
136        ] ];
137
138        $this->userOptionsManager->setOption( $user, 'language', $this->getContext()->getLanguage()->getCode() );
139        $this->userOptionsManager->saveOptions( $user );
140
141        $this->getResult()->addValue( null, $this->getModuleName(), $output );
142    }
143
144    private function doDelete(): void {
145        $this->checkUserRightsAny( 'translate-sandboxmanage' );
146
147        $params = $this->extractRequestParams();
148
149        foreach ( $params['userid'] as $userId ) {
150            $user = $this->userFactory->newFromId( $userId );
151            $userPage = $user->getUserPage();
152
153            $this->translateSandbox->sendEmail( $this->getUser(), $user, 'rejection' );
154
155            try {
156                $this->translateSandbox->deleteUser( $user );
157            } catch ( UserNotSandboxedException $e ) {
158                $this->dieWithError(
159                    [ 'apierror-translate-sandbox-invalidparam', wfEscapeWikiText( $e->getMessage() ) ],
160                    'invalidparam'
161                );
162            }
163
164            $logEntry = new ManualLogEntry( 'translatorsandbox', 'rejected' );
165            $logEntry->setPerformer( $this->getUser() );
166            $logEntry->setTarget( $userPage );
167            $logId = $logEntry->insert();
168            $logEntry->publish( $logId );
169        }
170    }
171
172    private function doPromote(): void {
173        $this->checkUserRightsAny( 'translate-sandboxmanage' );
174
175        $params = $this->extractRequestParams();
176
177        foreach ( $params['userid'] as $userId ) {
178            $user = $this->userFactory->newFromId( $userId );
179
180            try {
181                $this->translateSandbox->promoteUser( $user );
182            } catch ( UserNotSandboxedException $e ) {
183                $this->dieWithError(
184                    [ 'apierror-translate-sandbox-invalidparam', wfEscapeWikiText( $e->getMessage() ) ],
185                    'invalidparam'
186                );
187            }
188
189            $this->translateSandbox->sendEmail( $this->getUser(), $user, 'promotion' );
190
191            $logEntry = new ManualLogEntry( 'translatorsandbox', 'promoted' );
192            $logEntry->setPerformer( $this->getUser() );
193            $logEntry->setTarget( $user->getUserPage() );
194            $logEntry->setParameters( [
195                '4::userid' => $user->getId(),
196            ] );
197            $logId = $logEntry->insert();
198            $logEntry->publish( $logId );
199
200            $this->createUserPage( $user );
201        }
202    }
203
204    private function doRemind(): void {
205        $params = $this->extractRequestParams();
206
207        foreach ( $params['userid'] as $userId ) {
208            $target = $this->userFactory->newFromId( $userId );
209
210            try {
211                $this->translateSandbox->sendEmail( $this->getUser(), $target, 'reminder' );
212            } catch ( UserNotSandboxedException $e ) {
213                $this->dieWithError(
214                    [ 'apierror-translate-sandbox-invalidparam', wfEscapeWikiText( $e->getMessage() ) ],
215                    'invalidparam'
216                );
217            }
218        }
219    }
220
221    /** Create a user page for a user with a babel template based on the signup preferences. */
222    private function createUserPage( User $user ): void {
223        $userPage = $user->getUserPage();
224
225        if ( $userPage->exists() ) {
226            return;
227        }
228
229        $languagePreferences = FormatJson::decode(
230            $this->userOptionsLookup->getOption( $user, 'translate-sandbox' ),
231            true
232        );
233        $languages = implode( '|', $languagePreferences[ 'languages' ] ?? [] );
234        $babelText = "{{#babel:$languages}}";
235        $summary = $this->msg( 'tsb-create-user-page' )->inContentLanguage()->text();
236
237        $page = $this->wikiPageFactory->newFromTitle( $userPage );
238        $content = ContentHandler::makeContent( $babelText, $userPage );
239
240        $page->newPageUpdater( $user )
241            ->setContent( SlotRecord::MAIN, $content )
242            ->saveRevision( CommentStoreComment::newUnsavedComment( trim( $summary ) ), EDIT_NEW );
243    }
244
245    public function isWriteMode(): bool {
246        return true;
247    }
248
249    public function needsToken(): string {
250        return 'csrf';
251    }
252
253    protected function getAllowedParams(): array {
254        return [
255            'do' => [
256                ParamValidator::PARAM_TYPE => [ 'create', 'delete', 'promote', 'remind' ],
257                ParamValidator::PARAM_REQUIRED => true,
258            ],
259            'userid' => [
260                ParamValidator::PARAM_TYPE => 'integer',
261                ParamValidator::PARAM_DEFAULT => 0,
262                ParamValidator::PARAM_ISMULTI => true,
263            ],
264            'username' => [ ParamValidator::PARAM_TYPE => 'string' ],
265            'password' => [ ParamValidator::PARAM_TYPE => 'string' ],
266            'email' => [ ParamValidator::PARAM_TYPE => 'string' ],
267        ];
268    }
269}