wikimedia/mediawiki-extensions-Translate

View on GitHub
src/TranslatorSandbox/TranslatorSandboxActionApi.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php
declare( strict_types = 1 );

namespace MediaWiki\Extension\Translate\TranslatorSandbox;

use ApiBase;
use ApiMain;
use ContentHandler;
use FormatJson;
use ManualLogEntry;
use MediaWiki\CommentStore\CommentStoreComment;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Page\WikiPageFactory;
use MediaWiki\Parser\Sanitizer;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\User\Options\UserOptionsLookup;
use MediaWiki\User\Options\UserOptionsManager;
use MediaWiki\User\User;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserNameUtils;
use RuntimeException;
use Wikimedia\ParamValidator\ParamValidator;

/**
 * WebAPI for the sandbox feature of Translate.
 * @author Niklas Laxström
 * @license GPL-2.0-or-later
 * @ingroup API TranslateAPI
 */
class TranslatorSandboxActionApi extends ApiBase {
    private UserFactory $userFactory;
    private UserNameUtils $userNameUtils;
    private UserOptionsManager $userOptionsManager;
    private WikiPageFactory $wikiPageFactory;
    private UserOptionsLookup $userOptionsLookup;
    private TranslateSandbox $translateSandbox;
    private bool $isSandboxEnabled;
    public const CONSTRUCTOR_OPTIONS = [
        'TranslateUseSandbox',
    ];

    public function __construct(
        ApiMain $mainModule,
        string $moduleName,
        UserFactory $userFactory,
        UserNameUtils $userNameUtils,
        UserOptionsManager $userOptionsManager,
        WikiPageFactory $wikiPageFactory,
        UserOptionsLookup $userOptionsLookup,
        TranslateSandbox $translateSandbox,
        ServiceOptions $options
    ) {
        parent::__construct( $mainModule, $moduleName );
        $this->userFactory = $userFactory;
        $this->userNameUtils = $userNameUtils;
        $this->userOptionsManager = $userOptionsManager;
        $this->wikiPageFactory = $wikiPageFactory;
        $this->userOptionsLookup = $userOptionsLookup;
        $this->translateSandbox = $translateSandbox;
        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
        $this->isSandboxEnabled = $options->get( 'TranslateUseSandbox' );
    }

    public function execute(): void {
        if ( !$this->isSandboxEnabled ) {
            $this->dieWithError( 'apierror-translate-sandboxdisabled', 'sandboxdisabled' );
        }

        $params = $this->extractRequestParams();
        switch ( $params['do'] ) {
            case 'create':
                $this->doCreate();
                break;
            case 'delete':
                $this->doDelete();
                break;
            case 'promote':
                $this->doPromote();
                break;
            case 'remind':
                $this->doRemind();
                break;
            default:
                $this->dieWithError( [ 'apierror-badparameter', 'do' ] );
        }
    }

    private function doCreate(): void {
        $params = $this->extractRequestParams();

        // Do validations
        foreach ( explode( '|', 'username|password|email' ) as $field ) {
            if ( !isset( $params[$field] ) ) {
                $this->dieWithError( [ 'apierror-missingparam', $field ], 'missingparam' );
            }
        }

        $username = $params['username'];

        $canonicalName = $this->userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_CREATABLE );

        if ( $canonicalName === false ) {
            $this->dieWithError( 'noname', 'invalidusername' );
        }

        $user = $this->userFactory->newFromName( $username );
        if ( $user->getId() !== 0 ) {
            $this->dieWithError( 'userexists', 'nonfreeusername' );
        }

        $password = $params['password'];
        $passwordValidityStatus = $user->checkPasswordValidity( $password );
        if ( !$passwordValidityStatus->isGood() ) {
            $this->dieStatus( $passwordValidityStatus );
        }

        $email = $params['email'];
        if ( !Sanitizer::validateEmail( $email ) ) {
            $this->dieWithError( 'invalidemailaddress', 'invalidemail' );
        }

        try {
            $user = $this->translateSandbox->addUser( $username, $email, $password );
        } catch ( RuntimeException $e ) {
            // Do not log this error as it might leak private information
            if ( $e->getCode() === TranslateSandbox::USER_CREATION_FAILURE ) {
                $this->dieWithError( 'apierror-translate-sandbox-user-add' );
            }

            throw $e;
        }

        $output = [ 'user' => [
            'name' => $user->getName(),
            'id' => $user->getId(),
        ] ];

        $this->userOptionsManager->setOption( $user, 'language', $this->getContext()->getLanguage()->getCode() );
        $this->userOptionsManager->saveOptions( $user );

        $this->getResult()->addValue( null, $this->getModuleName(), $output );
    }

    private function doDelete(): void {
        $this->checkUserRightsAny( 'translate-sandboxmanage' );

        $params = $this->extractRequestParams();

        foreach ( $params['userid'] as $userId ) {
            $user = $this->userFactory->newFromId( $userId );
            $userPage = $user->getUserPage();

            $this->translateSandbox->sendEmail( $this->getUser(), $user, 'rejection' );

            try {
                $this->translateSandbox->deleteUser( $user );
            } catch ( UserNotSandboxedException $e ) {
                $this->dieWithError(
                    [ 'apierror-translate-sandbox-invalidparam', wfEscapeWikiText( $e->getMessage() ) ],
                    'invalidparam'
                );
            }

            $logEntry = new ManualLogEntry( 'translatorsandbox', 'rejected' );
            $logEntry->setPerformer( $this->getUser() );
            $logEntry->setTarget( $userPage );
            $logId = $logEntry->insert();
            $logEntry->publish( $logId );
        }
    }

    private function doPromote(): void {
        $this->checkUserRightsAny( 'translate-sandboxmanage' );

        $params = $this->extractRequestParams();

        foreach ( $params['userid'] as $userId ) {
            $user = $this->userFactory->newFromId( $userId );

            try {
                $this->translateSandbox->promoteUser( $user );
            } catch ( UserNotSandboxedException $e ) {
                $this->dieWithError(
                    [ 'apierror-translate-sandbox-invalidparam', wfEscapeWikiText( $e->getMessage() ) ],
                    'invalidparam'
                );
            }

            $this->translateSandbox->sendEmail( $this->getUser(), $user, 'promotion' );

            $logEntry = new ManualLogEntry( 'translatorsandbox', 'promoted' );
            $logEntry->setPerformer( $this->getUser() );
            $logEntry->setTarget( $user->getUserPage() );
            $logEntry->setParameters( [
                '4::userid' => $user->getId(),
            ] );
            $logId = $logEntry->insert();
            $logEntry->publish( $logId );

            $this->createUserPage( $user );
        }
    }

    private function doRemind(): void {
        $params = $this->extractRequestParams();

        foreach ( $params['userid'] as $userId ) {
            $target = $this->userFactory->newFromId( $userId );

            try {
                $this->translateSandbox->sendEmail( $this->getUser(), $target, 'reminder' );
            } catch ( UserNotSandboxedException $e ) {
                $this->dieWithError(
                    [ 'apierror-translate-sandbox-invalidparam', wfEscapeWikiText( $e->getMessage() ) ],
                    'invalidparam'
                );
            }
        }
    }

    /** Create a user page for a user with a babel template based on the signup preferences. */
    private function createUserPage( User $user ): void {
        $userPage = $user->getUserPage();

        if ( $userPage->exists() ) {
            return;
        }

        $languagePreferences = FormatJson::decode(
            $this->userOptionsLookup->getOption( $user, 'translate-sandbox' ),
            true
        );
        $languages = implode( '|', $languagePreferences[ 'languages' ] ?? [] );
        $babelText = "{{#babel:$languages}}";
        $summary = $this->msg( 'tsb-create-user-page' )->inContentLanguage()->text();

        $page = $this->wikiPageFactory->newFromTitle( $userPage );
        $content = ContentHandler::makeContent( $babelText, $userPage );

        $page->newPageUpdater( $user )
            ->setContent( SlotRecord::MAIN, $content )
            ->saveRevision( CommentStoreComment::newUnsavedComment( trim( $summary ) ), EDIT_NEW );
    }

    public function isWriteMode(): bool {
        return true;
    }

    public function needsToken(): string {
        return 'csrf';
    }

    protected function getAllowedParams(): array {
        return [
            'do' => [
                ParamValidator::PARAM_TYPE => [ 'create', 'delete', 'promote', 'remind' ],
                ParamValidator::PARAM_REQUIRED => true,
            ],
            'userid' => [
                ParamValidator::PARAM_TYPE => 'integer',
                ParamValidator::PARAM_DEFAULT => 0,
                ParamValidator::PARAM_ISMULTI => true,
            ],
            'token' => [
                ParamValidator::PARAM_TYPE => 'string',
                ParamValidator::PARAM_REQUIRED => true,
            ],
            'username' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'password' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'email' => [ ParamValidator::PARAM_TYPE => 'string' ],
        ];
    }
}