wikimedia/mediawiki-extensions-Wikibase

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

Summary

Maintainability
D
3 days
Test Coverage
<?php

namespace Wikibase\Repo\Specials;

use MediaWiki\Output\OutputPage;
use MediaWiki\Status\Status;
use Wikibase\DataModel\Entity\EntityDocument;
use Wikibase\DataModel\Entity\Property;
use Wikibase\DataModel\Term\Term;
use Wikibase\Lib\DataTypeFactory;
use Wikibase\Lib\SettingsArray;
use Wikibase\Lib\Store\EntityNamespaceLookup;
use Wikibase\Lib\Store\EntityTitleLookup;
use Wikibase\Lib\Summary;
use Wikibase\Repo\AnonymousEditWarningBuilder;
use Wikibase\Repo\CopyrightMessageBuilder;
use Wikibase\Repo\DataTypeSelector;
use Wikibase\Repo\EditEntity\MediaWikiEditEntityFactory;
use Wikibase\Repo\Specials\HTMLForm\HTMLAliasesField;
use Wikibase\Repo\Specials\HTMLForm\HTMLContentLanguageField;
use Wikibase\Repo\Specials\HTMLForm\HTMLTrimmedTextField;
use Wikibase\Repo\Store\TermsCollisionDetector;
use Wikibase\Repo\SummaryFormatter;
use Wikibase\Repo\Validators\TermValidatorFactory;
use Wikibase\Repo\Validators\ValidatorErrorLocalizer;

/**
 * Page for creating new Wikibase properties.
 *
 * @license GPL-2.0-or-later
 * @author John Erling Blad < jeblad@gmail.com >
 */
class SpecialNewProperty extends SpecialNewEntity {
    public const FIELD_LANG = 'lang';
    public const FIELD_DATATYPE = 'datatype';
    public const FIELD_LABEL = 'label';
    public const FIELD_DESCRIPTION = 'description';
    public const FIELD_ALIASES = 'aliases';

    private AnonymousEditWarningBuilder $anonymousEditWarningBuilder;

    /** @var DataTypeFactory */
    private $dataTypeFactory;

    /** @var TermValidatorFactory */
    private $termValidatorFactory;

    /**
     * @var TermsCollisionDetector
     */
    private $termsCollisionDetector;

    /** @var ValidatorErrorLocalizer */
    private $errorLocalizer;

    public function __construct(
        array $tags,
        SpecialPageCopyrightView $specialPageCopyrightView,
        EntityNamespaceLookup $entityNamespaceLookup,
        SummaryFormatter $summaryFormatter,
        EntityTitleLookup $entityTitleLookup,
        MediaWikiEditEntityFactory $editEntityFactory,
        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
        DataTypeFactory $dataTypeFactory,
        TermValidatorFactory $termValidatorFactory,
        TermsCollisionDetector $termsCollisionDetector,
        ValidatorErrorLocalizer $errorLocalizer,
        bool $isMobileView
    ) {
        parent::__construct(
            'NewProperty',
            'property-create',
            $tags,
            $specialPageCopyrightView,
            $entityNamespaceLookup,
            $summaryFormatter,
            $entityTitleLookup,
            $editEntityFactory,
            $isMobileView
        );

        $this->anonymousEditWarningBuilder = $anonymousEditWarningBuilder;
        $this->dataTypeFactory = $dataTypeFactory;
        $this->termValidatorFactory = $termValidatorFactory;
        $this->termsCollisionDetector = $termsCollisionDetector;
        $this->errorLocalizer = $errorLocalizer;
    }

    public static function factory(
        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
        DataTypeFactory $dataTypeFactory,
        MediaWikiEditEntityFactory $editEntityFactory,
        EntityNamespaceLookup $entityNamespaceLookup,
        EntityTitleLookup $entityTitleLookup,
        bool $isMobileView,
        TermsCollisionDetector $propertyTermsCollisionDetector,
        SettingsArray $repoSettings,
        SummaryFormatter $summaryFormatter,
        TermValidatorFactory $termValidatorFactory,
        ValidatorErrorLocalizer $errorLocalizer
    ): self {
        $copyrightView = new SpecialPageCopyrightView(
            new CopyrightMessageBuilder(),
            $repoSettings->getSetting( 'dataRightsUrl' ),
            $repoSettings->getSetting( 'dataRightsText' )
        );

        return new self(
            $repoSettings->getSetting( 'specialPageTags' ),
            $copyrightView,
            $entityNamespaceLookup,
            $summaryFormatter,
            $entityTitleLookup,
            $editEntityFactory,
            $anonymousEditWarningBuilder,
            $dataTypeFactory,
            $termValidatorFactory,
            $propertyTermsCollisionDetector,
            $errorLocalizer,
            $isMobileView
        );
    }

    /**
     * @see SpecialNewEntity::doesWrites
     *
     * @return bool
     */
    public function doesWrites() {
        return true;
    }

    /**
     * @see SpecialNewEntity::createEntityFromFormData
     *
     * @param array $formData
     *
     * @return Property
     */
    protected function createEntityFromFormData( array $formData ) {
        $languageCode = $formData[ self::FIELD_LANG ];

        $property = Property::newFromType( $formData[ self::FIELD_DATATYPE ] );

        $property->setLabel( $languageCode, $formData[ self::FIELD_LABEL ] );
        $property->setDescription( $languageCode, $formData[ self::FIELD_DESCRIPTION ] );

        $property->setAliases( $languageCode, $formData[ self::FIELD_ALIASES ] );

        return $property;
    }

    private function dataTypeExists( string $dataType ): bool {
        return in_array( $dataType, $this->dataTypeFactory->getTypeIds() );
    }

    /**
     * @see SpecialNewEntity::getFormFields()
     *
     * @return array[]
     */
    protected function getFormFields() {
        $formFields = [
            self::FIELD_LANG => [
                'name' => self::FIELD_LANG,
                'class' => HTMLContentLanguageField::class,
                'id' => 'wb-newentity-language',
            ],
            self::FIELD_LABEL => [
                'name' => self::FIELD_LABEL,
                'default' => $this->parts[0] ?? '',
                'class' => HTMLTrimmedTextField::class,
                'id' => 'wb-newentity-label',
                'placeholder-message' => 'wikibase-label-edit-placeholder',
                'label-message' => 'wikibase-newentity-label',
            ],
            self::FIELD_DESCRIPTION => [
                'name' => self::FIELD_DESCRIPTION,
                'default' => $this->parts[1] ?? '',
                'class' => HTMLTrimmedTextField::class,
                'id' => 'wb-newentity-description',
                'placeholder-message' => 'wikibase-description-edit-placeholder',
                'label-message' => 'wikibase-newentity-description',
            ],
            self::FIELD_ALIASES => [
                'name' => self::FIELD_ALIASES,
                'class' => HTMLAliasesField::class,
                'id' => 'wb-newentity-aliases',
            ],
        ];

        $selector = new DataTypeSelector(
            $this->dataTypeFactory->getTypes(),
            $this->getLanguage()->getCode()
        );

        $options = [
            $this->msg( 'wikibase-newproperty-pick-data-type' )->text() => '',
        ];
        $formFields[ self::FIELD_DATATYPE ] = [
            'name' => self::FIELD_DATATYPE,
            'type' => 'select',
            'default' => $this->parts[2] ?? '',
            'options' => array_merge( $options, $selector->getOptionsArray() ),
            'id' => 'wb-newproperty-datatype',
            'validation-callback' => function ( $dataType, $formData, $form ) {
                if ( !$this->dataTypeExists( $dataType ) ) {
                    return [ $this->msg( 'wikibase-newproperty-invalid-datatype' )->text() ];
                }

                return true;
            },
            'label-message' => 'wikibase-newproperty-datatype',
        ];

        return $formFields;
    }

    /**
     * @inheritDoc
     */
    protected function getLegend() {
        return $this->msg( 'wikibase-newproperty-fieldset' );
    }

    /**
     * @see SpecialNewEntity::getWarnings
     *
     * @return string[]
     */
    protected function getWarnings() {
        if ( !$this->getUser()->isRegistered() ) {
            return [
                $this->anonymousEditWarningBuilder->buildAnonymousEditWarningHTML( $this->getFullTitle()->getPrefixedText() ),
            ];
        }

        return [];
    }

    /**
     * @param array $formData
     *
     * @return Status
     */
    protected function validateFormData( array $formData ) {
        $status = Status::newGood();

        if ( $formData[ self::FIELD_LABEL ] == ''
             && $formData[ self::FIELD_DESCRIPTION ] == ''
             && $formData[ self::FIELD_ALIASES ] === []
        ) {
            $status->fatal( 'wikibase-newproperty-insufficient-data' );
        }

        if ( $formData[ self::FIELD_LABEL ] !== '' &&
            $formData[ self::FIELD_LABEL ] === $formData[ self::FIELD_DESCRIPTION ]
        ) {
            $status->fatal( 'wikibase-newproperty-same-label-and-description' );
        }

        if ( $formData[self::FIELD_LABEL] != '' ) {
            $validator = $this->termValidatorFactory->getLabelValidator( $this->getEntityType() );
            $result = $validator->validate( $formData[self::FIELD_LABEL] );
            $status->merge( $this->errorLocalizer->getResultStatus( $result ) );

            $validator = $this->termValidatorFactory->getLabelLanguageValidator();
            $result = $validator->validate( $formData[self::FIELD_LANG] );
            $status->merge( $this->errorLocalizer->getResultStatus( $result ) );
        }

        if ( $formData[self::FIELD_DESCRIPTION] != '' ) {
            $validator = $this->termValidatorFactory->getDescriptionValidator();
            $result = $validator->validate( $formData[self::FIELD_DESCRIPTION] );
            $status->merge( $this->errorLocalizer->getResultStatus( $result ) );

            $validator = $this->termValidatorFactory->getDescriptionLanguageValidator();
            $result = $validator->validate( $formData[self::FIELD_LANG] );
            $status->merge( $this->errorLocalizer->getResultStatus( $result ) );
        }

        if ( $formData[self::FIELD_ALIASES] !== [] ) {
            $validator = $this->termValidatorFactory->getAliasValidator();
            foreach ( $formData[self::FIELD_ALIASES] as $alias ) {
                $result = $validator->validate( $alias );
                $status->merge( $this->errorLocalizer->getResultStatus( $result ) );
            }

            $result = $validator->validate( implode( '|', $formData[self::FIELD_ALIASES] ) );
            $status->merge( $this->errorLocalizer->getResultStatus( $result ) );

            $validator = $this->termValidatorFactory->getAliasLanguageValidator();
            $result = $validator->validate( $formData[self::FIELD_LANG] );
            $status->merge( $this->errorLocalizer->getResultStatus( $result ) );
        }

        // property label uniqueness is also checked later in LabelUniquenessValidator (T289473),
        // but we repeat it here to avoid consuming a property ID if there is a collision
        if ( $status->isOK() ) { // only do this more expensive check if everything else is OK
            $collidingPropertyId = $this->termsCollisionDetector->detectLabelCollision(
                $formData[self::FIELD_LANG],
                $formData[self::FIELD_LABEL]
            );
            if ( $collidingPropertyId !== null ) {
                $status->fatal(
                    'wikibase-validator-label-conflict',
                    $formData[self::FIELD_LABEL],
                    $formData[self::FIELD_LANG],
                    $collidingPropertyId
                );
            }
        }

        return $status;
    }

    /**
     * @param Property $property
     *
     * @return Summary
     * @suppress PhanParamSignatureMismatch Uses intersection types
     */
    protected function createSummary( EntityDocument $property ) {
        $uiLanguageCode = $this->getLanguage()->getCode();

        $summary = new Summary( 'wbeditentity', 'create' );
        $summary->setLanguage( $uiLanguageCode );
        /** @var Term|null $labelTerm */
        $labelTerm = $property->getLabels()->getIterator()->current();
        /** @var Term|null $descriptionTerm */
        $descriptionTerm = $property->getDescriptions()->getIterator()->current();
        $summary->addAutoSummaryArgs(
            $labelTerm ? $labelTerm->getText() : '',
            $descriptionTerm ? $descriptionTerm->getText() : ''
        );

        return $summary;
    }

    protected function displayBeforeForm( OutputPage $output ) {
        parent::displayBeforeForm( $output );
        $output->addModules( 'wikibase.special.languageLabelDescriptionAliases' );
    }

    /**
     * @inheritDoc
     */
    protected function getEntityType() {
        return Property::ENTITY_TYPE;
    }

}