wikimedia/mediawiki-extensions-Wikibase

View on GitHub
repo/includes/Specials/SpecialSetLabelDescriptionAliases.php

Summary

Maintainability
D
2 days
Test Coverage
<?php

namespace Wikibase\Repo\Specials;

use InvalidArgumentException;
use MediaWiki\Config\Config;
use MediaWiki\Html\Html;
use MediaWiki\HTMLForm\HTMLForm;
use MediaWiki\Languages\LanguageNameUtils;
use MediaWiki\Logger\LoggerFactory;
use Wikibase\DataModel\Entity\EntityDocument;
use Wikibase\DataModel\Term\Fingerprint;
use Wikibase\DataModel\Term\FingerprintProvider;
use Wikibase\Lib\ContentLanguages;
use Wikibase\Lib\SettingsArray;
use Wikibase\Lib\Store\EntityTitleLookup;
use Wikibase\Lib\Summary;
use Wikibase\Lib\UserInputException;
use Wikibase\Repo\AnonymousEditWarningBuilder;
use Wikibase\Repo\ChangeOp\ChangeOp;
use Wikibase\Repo\ChangeOp\ChangeOpException;
use Wikibase\Repo\ChangeOp\ChangeOpFactoryProvider;
use Wikibase\Repo\ChangeOp\ChangeOps;
use Wikibase\Repo\ChangeOp\FingerprintChangeOpFactory;
use Wikibase\Repo\CopyrightMessageBuilder;
use Wikibase\Repo\EditEntity\MediaWikiEditEntityFactory;
use Wikibase\Repo\Store\EntityPermissionChecker;
use Wikibase\Repo\SummaryFormatter;

/**
 * Special page for setting label, description and aliases of a Wikibase entity that features
 * labels, descriptions and aliases.
 *
 * @license GPL-2.0-or-later
 * @author Thiemo Kreuz
 */
class SpecialSetLabelDescriptionAliases extends SpecialModifyEntity {

    use ParameterizedDescriptionTrait;

    public const BUTTON_MESSAGE_PUBLISH = 'publishchanges';
    public const BUTTON_MESSAGE_SAVE = 'savechanges';

    /**
     * @var FingerprintChangeOpFactory
     */
    private $changeOpFactory;

    /**
     * @var ContentLanguages
     */
    private $termsLanguages;

    /**
     * @var EntityPermissionChecker
     */
    private $permissionChecker;

    /**
     * @var LanguageNameUtils
     */
    private $languageNameUtils;

    /**
     * @var string
     */
    private $submitButtonMessage;

    /**
     * @var string|null
     */
    private $languageCode;

    /**
     * @var string
     */
    private $label = '';

    /**
     * @var string
     */
    private $description = '';

    /**
     * @var string[]
     */
    private $aliases = [];

    public function __construct(
        array $tags,
        SpecialPageCopyrightView $copyrightView,
        SummaryFormatter $summaryFormatter,
        EntityTitleLookup $entityTitleLookup,
        MediaWikiEditEntityFactory $editEntityFactory,
        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
        FingerprintChangeOpFactory $changeOpFactory,
        ContentLanguages $termsLanguages,
        EntityPermissionChecker $permissionChecker,
        LanguageNameUtils $languageNameUtils,
        string $submitButtonMessage
    ) {
        parent::__construct(
            'SetLabelDescriptionAliases',
            $tags,
            $copyrightView,
            $summaryFormatter,
            $entityTitleLookup,
            $editEntityFactory,
            $anonymousEditWarningBuilder,
        );

        $this->changeOpFactory = $changeOpFactory;
        $this->termsLanguages = $termsLanguages;
        $this->permissionChecker = $permissionChecker;
        $this->languageNameUtils = $languageNameUtils;
        $this->submitButtonMessage = $submitButtonMessage;
    }

    public static function factory(
        LanguageNameUtils $languageNameUtils,
        Config $mwConfig,
        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
        ChangeOpFactoryProvider $changeOpFactoryProvider,
        MediaWikiEditEntityFactory $editEntityFactory,
        EntityPermissionChecker $entityPermissionChecker,
        EntityTitleLookup $entityTitleLookup,
        SettingsArray $repoSettings,
        SummaryFormatter $summaryFormatter,
        ContentLanguages $termsLanguages
    ): self {
        $copyrightView = new SpecialPageCopyrightView(
            new CopyrightMessageBuilder(),
            $repoSettings->getSetting( 'dataRightsUrl' ),
            $repoSettings->getSetting( 'dataRightsText' )
        );

        return new self(
            $repoSettings->getSetting( 'specialPageTags' ),
            $copyrightView,
            $summaryFormatter,
            $entityTitleLookup,
            $editEntityFactory,
            $anonymousEditWarningBuilder,
            $changeOpFactoryProvider->getFingerprintChangeOpFactory(),
            $termsLanguages,
            $entityPermissionChecker,
            $languageNameUtils,
            $mwConfig->get( 'EditSubmitButtonLabelPublish' ) ? self::BUTTON_MESSAGE_PUBLISH : self::BUTTON_MESSAGE_SAVE
        );
    }

    public function doesWrites() {
        return true;
    }

    /**
     * @see SpecialModifyEntity::validateInput
     *
     * @return bool
     */
    protected function validateInput() {
        return parent::validateInput()
            && $this->getBaseRevision()->getEntity() instanceof FingerprintProvider
            && $this->isValidLanguageCode( $this->languageCode )
            && $this->wasPostedWithLabelDescriptionOrAliases()
            && $this->isAllowedToChangeTerms( $this->getBaseRevision()->getEntity() );
    }

    /**
     * @return bool
     */
    private function wasPostedWithLabelDescriptionOrAliases() {
        $request = $this->getRequest();

        return $request->wasPosted() && (
            $request->getCheck( 'label' )
            || $request->getCheck( 'description' )
            || $request->getCheck( 'aliases' )
        );
    }

    /**
     * @param EntityDocument $entity
     *
     * @return bool
     */
    private function isAllowedToChangeTerms( EntityDocument $entity ) {
        $status = $this->permissionChecker->getPermissionStatusForEntity(
            $this->getUser(),
            EntityPermissionChecker::ACTION_EDIT_TERMS,
            $entity
        );

        if ( !$status->isOK() ) {
            $this->showErrorHTML( $this->msg( 'permissionserrors' )->parse() );
            return false;
        }

        return true;
    }

    /**
     * @see SpecialModifyEntity::getForm
     *
     * @param EntityDocument|null $entity
     *
     * @return HTMLForm
     */
    protected function getForm( EntityDocument $entity = null ) {
        if ( $this->isEditFormStep( $entity ) ) {

            $languageName = $this->languageNameUtils->getLanguageName(
                $this->languageCode, $this->getLanguage()->getCode()
            );
            $intro = $this->msg(
                'wikibase-setlabeldescriptionaliases-introfull',
                $this->getEntityTitle( $entity->getId() )->getPrefixedText(),
                $languageName
            )->parse();

            $formDescriptor = [
                'id' => [
                    'name' => 'id',
                    'type' => 'hidden',
                    'default' => $entity->getId()->getSerialization(),
                ],
                'language' => [
                    'name' => 'language',
                    'type' => 'hidden',
                    'default' => $this->languageCode,
                ],
                'revid' => [
                    'name' => 'revid',
                    'type' => 'hidden',
                    'default' => $this->getBaseRevision()->getRevisionId(),
                ],
            ];
            $formDescriptor = array_merge(
                $formDescriptor,
                $this->getLabeledInputField( 'label', $this->label ),
                $this->getDescriptionInputField( $this->languageCode, $this->description ),
                $this->getLabeledInputField( 'aliases', implode( '|', $this->aliases ) )
            );
        } else {
            $intro = $this->msg( 'wikibase-setlabeldescriptionaliases-intro' )->parse();
            $fieldId = 'wikibase-setlabeldescriptionaliases-language';
            $languageCode = $this->languageCode ?: $this->getLanguage()->getCode();

            $formDescriptor = $this->getFormElements( $entity );
            $formDescriptor['language'] = [
                'name' => 'language',
                'default' => $languageCode,
                'type' => 'text',
                'id' => $fieldId,
                'label-message' => 'wikibase-modifyterm-language',
            ];
        }

        return HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
            ->setHeaderHtml( Html::rawElement( 'p', [], $intro ) );
    }

    private function getDescriptionInputField( string $languageCode, string $value ): array {
        $fieldDescription = $this->getLabeledInputField( 'description', $value );

        // We don't support "mul" descriptions (T330193).
        if ( $languageCode === 'mul' ) {
            $fieldDescription['description']['disabled'] = true;
            $fieldDescription['description']['notices'] = [
                $this->msg(
                    'wikibase-setlabeldescriptionaliases-description-not-supported'
                )->text(),
            ];
        }

        return $fieldDescription;
    }

    /**
     * Returns an HTML label and text input element for a specific term.
     *
     * @param string $termType Either 'label', 'description' or 'aliases'.
     * @param string $value Text to fill the input element with
     *
     * @return array[]
     */
    private function getLabeledInputField( $termType, $value ): array {
        $fieldId = 'wikibase-setlabeldescriptionaliases-' . $termType;

        // Messages:
        // wikibase-setlabeldescriptionaliases-label-label
        // wikibase-setlabeldescriptionaliases-description-label
        // wikibase-setlabeldescriptionaliases-aliases-label
        return [
            $termType => [
                'name' => $termType,
                'default' => $value,
                'type' => 'text',
                'id' => $fieldId,
                'placeholder' => $value,
                'label-message' => $fieldId . '-label',
            ],
        ];
    }

    /**
     * @see SpecialModifyEntity::processArguments
     *
     * @param string|null $subPage
     */
    protected function processArguments( $subPage ) {
        $this->extractInput( $subPage );

        // Parse the 'id' parameter and throw an exception if the entity cannot be loaded
        parent::processArguments( $subPage );

        if ( $this->languageCode === '' ) {
            $this->languageCode = $this->getLanguage()->getCode();
        } elseif ( !$this->isValidLanguageCode( $this->languageCode ) ) {
            $msg = $this->msg( 'wikibase-wikibaserepopage-invalid-langcode' )
                ->plaintextParams( $this->languageCode );

            $this->showErrorHTML( $msg->parse() );
            $this->languageCode = null;
        }

        $entity = $this->getEntityForDisplay();
        if ( $this->languageCode !== null && $entity ) {
            if ( $entity instanceof FingerprintProvider ) {
                $this->setFingerprintFields( $entity->getFingerprint() );
            }
        }
    }

    /**
     * @param string|null $subPage
     */
    private function extractInput( $subPage ) {
        $request = $this->getRequest();

        $parts = $subPage ? explode( '/', $subPage, 2 ) : [];
        $this->languageCode = $request->getRawVal( 'language' ) ?? $parts[1] ?? '';

        $label = $request->getVal( 'label', '' );
        $this->label = $this->stringNormalizer->trimToNFC( $label );

        $description = $request->getVal( 'description', '' );
        $this->description = $this->stringNormalizer->trimToNFC( $description );

        $aliases = $request->getVal( 'aliases', '' );
        $aliases = $this->stringNormalizer->trimToNFC( $aliases );
        $this->aliases = $aliases === '' ? [] : explode( '|', $aliases );
        foreach ( $this->aliases as &$alias ) {
            $alias = $this->stringNormalizer->trimToNFC( $alias );
        }
    }

    private function setFingerprintFields( Fingerprint $fingerprint ) {
        if ( !$this->getRequest()->getCheck( 'label' )
            && $fingerprint->hasLabel( $this->languageCode )
        ) {
            $this->label = $fingerprint->getLabel( $this->languageCode )->getText();
        }

        if ( !$this->getRequest()->getCheck( 'description' )
            && $fingerprint->hasDescription( $this->languageCode )
        ) {
            $this->description = $fingerprint->getDescription( $this->languageCode )->getText();
        }

        if ( !$this->getRequest()->getCheck( 'aliases' )
            && $fingerprint->hasAliasGroup( $this->languageCode )
        ) {
            $this->aliases = $fingerprint->getAliasGroup( $this->languageCode )->getAliases();
        }
    }

    /**
     * @param string|null $languageCode
     *
     * @return bool
     */
    private function isValidLanguageCode( $languageCode ) {
        return $languageCode !== null && $this->termsLanguages->hasLanguage( $languageCode );
    }

    /**
     * @see SpecialModifyEntity::modifyEntity
     *
     * @param EntityDocument $entity
     *
     * @throws InvalidArgumentException
     * @return Summary|bool
     */
    protected function modifyEntity( EntityDocument $entity ) {
        if ( !( $entity instanceof FingerprintProvider ) ) {
            throw new InvalidArgumentException( '$entity must be a FingerprintProvider' );
        }

        if ( $this->assertNoPipeCharacterInAliases( $entity->getFingerprint() ) ) {
            $logger = LoggerFactory::getInstance( 'Wikibase' );
            $logger->error( 'Special:SpecialSetLabelDescriptionAliases attempt to save pipes in aliases' );
            $this->showErrorHTML( $this->msg( 'wikibase-wikibaserepopage-pipe-in-alias' )->parse() );
            return false;
        }
        $changeOps = $this->getChangeOps( $entity->getFingerprint() );

        if ( !$changeOps ) {
            return false;
        }

        try {
            return $this->applyChangeOpList( $changeOps, $entity );
        } catch ( ChangeOpException $ex ) {
            $this->showErrorHTML( $ex->getMessage() );
            return false;
        }
    }

    /**
     * @param Fingerprint $fingerprint
     *
     * @throws UserInputException
     * @return bool
     */
    private function assertNoPipeCharacterInAliases( Fingerprint $fingerprint ) {
        if ( $this->aliases ) {
            if ( $fingerprint->hasAliasGroup( $this->languageCode ) ) {
                $aliasesInLang = $fingerprint->getAliasGroup( $this->languageCode )->getAliases();
                foreach ( $aliasesInLang as $alias ) {
                    if ( strpos( $alias, '|' ) !== false ) {
                        return true;

                    }
                }
            }
        }

        return false;
    }

    /**
     * @throws ChangeOpException
     */
    private function applyChangeOpList( array $changeOps, EntityDocument $entity ): Summary {
        $changeOp = $this->changeOpFactory->newFingerprintChangeOp( new ChangeOps( $changeOps ) );
        /**
         * XXX: The $changeOps array is still used below as it is indexed with the
         * module name to pass to the Summary object.
         */
        if ( count( $changeOps ) === 1 ) {
            $module = key( $changeOps );
            $summary = new Summary( $module );
            $this->applyChangeOp( $changeOp, $entity, $summary );
            return $summary;
        } else {
            $this->applyChangeOp( $changeOp, $entity, new Summary() );
            return $this->getSummaryForLabelDescriptionAliases();
        }
    }

    /**
     * @param Fingerprint $fingerprint
     *
     * @return ChangeOp[]
     */
    private function getChangeOps( Fingerprint $fingerprint ) {
        $changeOpFactory = $this->changeOpFactory;
        $changeOps = [];

        if ( $this->label !== '' ) {
            if ( !$fingerprint->hasLabel( $this->languageCode )
                || $fingerprint->getLabel( $this->languageCode )->getText() !== $this->label
            ) {
                $changeOps['wbsetlabel'] = $changeOpFactory->newSetLabelOp(
                    $this->languageCode,
                    $this->label
                );
            }
        } elseif ( $fingerprint->hasLabel( $this->languageCode ) ) {
            $changeOps['wbsetlabel'] = $changeOpFactory->newRemoveLabelOp(
                $this->languageCode
            );
        }

        if ( $this->description !== '' ) {
            if ( !$fingerprint->hasDescription( $this->languageCode )
                || $fingerprint->getDescription( $this->languageCode )->getText() !== $this->description
            ) {
                $changeOps['wbsetdescription'] = $changeOpFactory->newSetDescriptionOp(
                    $this->languageCode,
                    $this->description
                );
            }
        } elseif ( $fingerprint->hasDescription( $this->languageCode ) ) {
            $changeOps['wbsetdescription'] = $changeOpFactory->newRemoveDescriptionOp(
                $this->languageCode
            );
        }

        if ( $this->aliases ) {
            if ( !$fingerprint->hasAliasGroup( $this->languageCode )
                || $fingerprint->getAliasGroup( $this->languageCode )->getAliases() !== $this->aliases
            ) {
                $changeOps['wbsetaliases'] = $changeOpFactory->newSetAliasesOp(
                    $this->languageCode,
                    $this->aliases
                );
            }
        } elseif ( $fingerprint->hasAliasGroup( $this->languageCode ) ) {
            $changeOps['wbsetaliases'] = $changeOpFactory->newRemoveAliasesOp(
                $this->languageCode,
                $fingerprint->getAliasGroup( $this->languageCode )->getAliases()
            );
        }

        return $changeOps;
    }

    /**
     * @return Summary
     */
    private function getSummaryForLabelDescriptionAliases() {
        // FIXME: Introduce more specific messages if only 2 of the 3 fields changed.
        $summary = new Summary( 'wbsetlabeldescriptionaliases' );
        $summary->addAutoSummaryArgs( $this->label, $this->description, $this->aliases );

        $summary->setLanguage( $this->languageCode );
        return $summary;
    }

    /**
     * @param EntityDocument|null $entity
     *
     * @return string submit message key
     */
    protected function getSubmitKey( EntityDocument $entity = null ) {
        if ( $this->isEditFormStep( $entity ) ) {
            return $this->submitButtonMessage;
        }

        return 'wikibase-setlabeldescriptionaliases-continue';
    }

    /**
     * @param EntityDocument|null $entity
     *
     * @return bool
     */
    protected function showCopyrightNotice( EntityDocument $entity = null ) {
        return $this->isEditFormStep( $entity );
    }

    private function isEditFormStep( EntityDocument $entity = null ) {
        return $entity !== null && $this->languageCode !== null;
    }
}