wikimedia/mediawiki-extensions-Translate

View on GitHub
src/PageTranslation/DeleteTranslatableBundleSpecialPage.php

Summary

Maintainability
C
1 day
Test Coverage
<?php
declare( strict_types = 1 );

namespace MediaWiki\Extension\Translate\PageTranslation;

use ErrorPageError;
use MediaWiki\Extension\Translate\MessageBundleTranslation\MessageBundle;
use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundleFactory;
use MediaWiki\Extension\Translate\Utilities\Utilities;
use MediaWiki\HTMLForm\HTMLForm;
use MediaWiki\Output\OutputPage;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Request\WebRequest;
use MediaWiki\SpecialPage\UnlistedSpecialPage;
use MediaWiki\Title\Title;
use PermissionsError;
use ReadOnlyError;
use Xml;

/**
 * Special page which enables deleting translations of translatable bundles and translation pages
 * @author Niklas Laxström
 * @license GPL-2.0-or-later
 * @ingroup SpecialPage PageTranslation
 */
class DeleteTranslatableBundleSpecialPage extends UnlistedSpecialPage {
    // Basic form parameters both as text and as titles
    private string $text;
    private ?Title $title;
    // Other form parameters
    /** There must be reason for everything. */
    private string $reason;
    /** Allow skipping non-translation subpages. */
    private bool $doSubpages = false;
    /** Contains the language code if we are working with translation page */
    private ?string $code;
    private PermissionManager $permissionManager;
    private TranslatableBundleDeleter $bundleDeleter;
    private TranslatableBundleFactory $bundleFactory;
    private string $entityType;
    private const PAGE_TITLE_MSG = [
        'messagebundle' => 'pt-deletepage-mb-title',
        'translatablepage' => 'pt-deletepage-tp-title',
        'translationpage' => 'pt-deletepage-lang-title'
    ];
    private const WRAPPER_LEGEND_MSG = [
        'messagebundle' => 'pt-deletepage-mb-legend',
        'translatablepage' => 'pt-deletepage-tp-title',
        'translationpage' => 'pt-deletepage-tp-legend'
    ];
    private const LOG_PAGE = [
        'messagebundle' => 'Special:Log/messagebundle',
        'translatablepage' => 'Special:Log/pagetranslation',
        'translationpage' => 'Special:Log/pagetranslation'
    ];

    public function __construct(
        PermissionManager $permissionManager,
        TranslatableBundleDeleter $bundleDeleter,
        TranslatableBundleFactory $bundleFactory
    ) {
        parent::__construct( 'PageTranslationDeletePage', 'pagetranslation' );
        $this->permissionManager = $permissionManager;
        $this->bundleFactory = $bundleFactory;
        $this->bundleDeleter = $bundleDeleter;
    }

    public function doesWrites() {
        return true;
    }

    public function execute( $par ) {
        $this->addHelpLink( 'Help:Deletion_and_undeletion' );

        $request = $this->getRequest();

        $par = (string)$par;

        // Yes, the use of getVal() and getText() is wanted, see bug T22365
        $this->text = $request->getVal( 'wpTitle', $par );
        $this->title = Title::newFromText( $this->text );
        $this->reason = $this->getDeleteReason( $request );
        $this->doSubpages = $request->getBool( 'subpages' );

        if ( !$this->doBasicChecks() ) {
            return;
        }

        $out = $this->getOutput();

        // Real stuff starts here
        $entityType = $this->identifyEntityType();
        if ( !$entityType ) {
            throw new ErrorPageError( 'pt-deletepage-invalid-title', 'pt-deletepage-invalid-text' );
        }
        $this->entityType = $entityType;

        if ( $this->isTranslation() ) {
            [ , $this->code ] = Utilities::figureMessage( $this->title->getText() );
        } else {
            $this->code = null;
        }

        $out->setPageTitleMsg(
            $this->msg( self::PAGE_TITLE_MSG[ $this->entityType ], $this->title->getPrefixedText() )
        );

        if ( !$this->getUser()->isAllowed( 'pagetranslation' ) ) {
            throw new PermissionsError( 'pagetranslation' );
        }

        // Is there really no better way to do this?
        $subactionText = $request->getText( 'subaction' );
        switch ( $subactionText ) {
            case $this->msg( 'pt-deletepage-action-check' )->text():
                $subaction = 'check';
                break;
            case $this->msg( 'pt-deletepage-action-perform' )->text():
                $subaction = 'perform';
                break;
            case $this->msg( 'pt-deletepage-action-other' )->text():
                $subaction = '';
                break;
            default:
                $subaction = '';
        }

        if ( $subaction === 'check' && $this->checkToken() && $request->wasPosted() ) {
            $this->showConfirmation();
        } elseif ( $subaction === 'perform' && $this->checkToken() && $request->wasPosted() ) {
            $this->performAction();
        } else {
            $this->showForm();
        }
    }

    /**
     * Do the basic checks whether moving is possible and whether
     * the input looks anywhere near sane.
     * @throws PermissionsError|ErrorPageError|ReadOnlyError
     */
    private function doBasicChecks(): bool {
        // Check rights
        if ( !$this->userCanExecute( $this->getUser() ) ) {
            $this->displayRestrictionError();
        }

        if ( $this->title === null ) {
            throw new ErrorPageError( 'notargettitle', 'notargettext' );
        }

        if ( !$this->title->exists() ) {
            throw new ErrorPageError( 'nopagetitle', 'nopagetext' );
        }

        $permissionStatus = $this->permissionManager->getPermissionStatus(
            'delete', $this->getUser(), $this->title
        );
        if ( !$permissionStatus->isOK() ) {
            throw new PermissionsError( 'delete', $permissionStatus );
        }

        # Check for database lock
        $this->checkReadOnly();

        // Let the caller know it's safe to continue
        return true;
    }

    /**
     * Checks token. Use before real actions happen. Have to use wpEditToken
     * for compatibility for SpecialMovepage.php.
     */
    private function checkToken(): bool {
        return $this->getContext()->getCsrfTokenSet()->matchTokenField( 'wpEditToken' );
    }

    /** The query form. */
    private function showForm(): void {
        $out = $this->getOutput();
        $out->addBacklinkSubtitle( $this->title );
        $out->addWikiMsg( 'pt-deletepage-intro', self::LOG_PAGE[ $this->entityType ] );

        $formDescriptor = $this->getCommonFormFields();

        HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
            ->setAction( $this->getPageTitle( $this->text )->getLocalURL() )
            ->setSubmitName( 'subaction' )
            ->setSubmitTextMsg( 'pt-deletepage-action-check' )
            ->setWrapperLegendMsg( 'pt-deletepage-any-legend' )
            ->prepareForm()
            ->displayForm( false );
    }

    /**
     * The second form, which still allows changing some things.
     * Lists all the action which would take place.
     */
    private function showConfirmation(): void {
        $out = $this->getOutput();
        $count = 0;
        $subpageCount = 0;

        $out->addBacklinkSubtitle( $this->title );
        $out->addWikiMsg( 'pt-deletepage-intro', self::LOG_PAGE[ $this->entityType ] );

        $subpages = $this->bundleDeleter->getPagesForDeletion( $this->title, $this->code, $this->isTranslation() );

        $out->wrapWikiMsg( '== $1 ==', 'pt-deletepage-list-pages' );

        if ( !$this->isTranslation() ) {
            $count++;
            $out->addWikiTextAsInterface(
                $this->getChangeLine( $this->title )
            );
        }

        $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-translation' );
        $lines = [];
        foreach ( $subpages[ 'translationPages' ] as $old ) {
            $count++;
            $lines[] = $this->getChangeLine( $old );
        }
        $this->listPages( $out, $lines );

        $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-section' );

        $lines = [];
        foreach ( $subpages[ 'translationUnitPages' ] as $old ) {
            $count++;
            $lines[] = $this->getChangeLine( $old );
        }
        $this->listPages( $out, $lines );

        if ( Utilities::allowsSubpages( $this->title ) ) {
            $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-other' );
            $subpages = $subpages[ 'normalSubpages' ];
            $lines = [];
            foreach ( $subpages as $old ) {
                $subpageCount++;
                $lines[] = $this->getChangeLine( $old );
            }
            $this->listPages( $out, $lines );
        }

        $totalPageCount = $count + $subpageCount;

        $out->addWikiTextAsInterface( "----\n" );
        $out->addWikiMsg(
            'pt-deletepage-list-count',
            $this->getLanguage()->formatNum( $totalPageCount ),
            $this->getLanguage()->formatNum( $subpageCount )
        );

        $formDescriptor = $this->getCommonFormFields();
        $formDescriptor['subpages'] = [
            'type' => 'check',
            'name' => 'subpages',
            'id' => 'mw-subpages',
            'label' => $this->msg( 'pt-deletepage-subpages' )->text(),
            'default' => $this->doSubpages,
        ];

        $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
        $htmlForm
            ->setWrapperLegendMsg(
                $this->msg( self::WRAPPER_LEGEND_MSG[ $this->entityType ], $this->title->getPrefixedText() )
            )
            ->setAction( $this->getPageTitle( $this->text )->getLocalURL() )
            ->setSubmitTextMsg( 'pt-deletepage-action-perform' )
            ->setSubmitName( 'subaction' )
            ->setSubmitDestructive()
            ->addButton( [
                'name' => 'subaction',
                'value' => $this->msg( 'pt-deletepage-action-other' )->text(),
            ] )
            ->prepareForm()
            ->displayForm( false );
    }

    /** @return string One line of wikitext, without trailing newline. */
    private function getChangeLine( Title $title ): string {
        return '* ' . $title->getPrefixedText();
    }

    private function performAction(): void {
        $this->bundleDeleter->deleteAsynchronously(
            $this->title,
            $this->isTranslation(),
            $this->getUser(),
            $this->bundleDeleter->getPagesForDeletion( $this->title, $this->code, $this->isTranslation() ),
            $this->doSubpages,
            $this->reason
        );

        $this->getOutput()->addWikiMsg( 'pt-deletepage-started', self::LOG_PAGE[ $this->entityType ] );
    }

    private function getCommonFormFields(): array {
        $dropdownOptions = $this->msg( 'deletereason-dropdown' )->inContentLanguage()->text();

        $options = Xml::listDropdownOptions(
            $dropdownOptions,
            [
                'other' => $this->msg( 'pt-deletepage-reason-other' )->inContentLanguage()->text()
            ]
        );

        return [
            'wpTitle' => [
                'type' => 'text',
                'name' => 'wpTitle',
                'label-message' => 'pt-deletepage-current',
                'size' => 30,
                'default' => $this->title->getPrefixedText(),
                'readonly' => true,
            ],
            'wpDeleteReasonList' => [
                'type' => 'select',
                'name' => 'wpDeleteReasonList',
                'label-message' => 'pt-deletepage-reason',
                'options' => $options,
            ],
            'wpReason' => [
                'type' => 'text',
                'name' => 'wpReason',
                'label-message' => 'pt-deletepage-reason-details',
                'default' => $this->reason,
            ]
        ];
    }

    private function listPages( OutputPage $out, array $lines ): void {
        if ( $lines ) {
            $out->addWikiTextAsInterface( implode( "\n", $lines ) );
        } else {
            $out->addWikiMsg( 'pt-deletepage-list-no-pages' );
        }
    }

    private function getDeleteReason( WebRequest $request ): string {
        $dropdownSelection = $request->getText( 'wpDeleteReasonList', 'other' );
        $reasonInput = $request->getText( 'wpReason' );

        if ( $dropdownSelection === 'other' ) {
            return $reasonInput;
        } elseif ( $reasonInput !== '' ) {
            // Entry from drop down menu + additional comment
            $separator = $this->msg( 'colon-separator' )->inContentLanguage()->text();
            return "$dropdownSelection$separator$reasonInput";
        } else {
            return $dropdownSelection;
        }
    }

    /** Indentify type of entity being deleted: messagebundle, translatablepage, or translations */
    private function identifyEntityType(): ?string {
        $bundle = $this->bundleFactory->getBundle( $this->title );
        if ( $bundle ) {
            if ( $bundle instanceof MessageBundle ) {
                return 'messagebundle';
            } else {
                return 'translatablepage';
            }
        } elseif ( TranslatablePage::isTranslationPage( $this->title ) ) {
            return 'translationpage';
        }

        return null;
    }

    private function isTranslation(): bool {
        return $this->entityType === 'translationpage';
    }
}