wikimedia/mediawiki-extensions-Wikibase

View on GitHub
repo/includes/Actions/SubmitEntityAction.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

declare( strict_types = 1 );

namespace Wikibase\Repo\Actions;

use Article;
use LogicException;
use MediaWiki\Context\DerivativeContext;
use MediaWiki\Context\IContextSource;
use MediaWiki\Page\WikiPageFactory;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Revision\RevisionLookup;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Status\Status;
use MediaWiki\Title\Title;
use MediaWiki\User\Options\UserOptionsLookup;
use MediaWiki\User\TempUser\TempUserCreator;
use MediaWiki\Watchlist\WatchlistManager;
use Wikibase\Repo\AnonymousEditWarningBuilder;
use Wikibase\Repo\Content\EntityContent;
use Wikibase\Repo\Diff\EntityDiffVisualizerFactory;
use Wikibase\Repo\EditEntity\EditFilterHookRunner;
use Wikibase\Repo\SummaryFormatter;
use Wikimedia\Assert\Assert;

/**
 * Handles the submit action for Wikibase entities.
 * This performs the undo and restore operations when requested.
 * Otherwise it will just show the normal entity view.
 *
 * @license GPL-2.0-or-later
 * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 * @author Jens Ohlig
 * @author Daniel Kinzler
 */
class SubmitEntityAction extends EditEntityAction {

    /**
     * {@link ObjectFactory} specification for this class,
     * to be returned by {@link EntityHandler::getActionOverrides()} implementations.
     */
    public const SPEC = [
        'class' => self::class,
        'services' => [
            'PermissionManager',
            'RevisionLookup',
            'TempUserCreator',
            'UserOptionsLookup',
            'WatchlistManager',
            'WikiPageFactory',
            'WikibaseRepo.AnonymousEditWarningBuilder',
            'WikibaseRepo.EditFilterHookRunner',
            'WikibaseRepo.EntityDiffVisualizerFactory',
            'WikibaseRepo.SummaryFormatter',
        ],
    ];

    private TempUserCreator $tempUserCreator;
    private UserOptionsLookup $userOptionsLookup;
    private WatchlistManager $watchlistManager;
    private WikiPageFactory $wikiPageFactory;
    private EditFilterHookRunner $editFilterHookRunner;

    public function __construct(
        Article $article,
        IContextSource $context,
        PermissionManager $permissionManager,
        RevisionLookup $revisionLookup,
        TempUserCreator $tempUserCreator,
        UserOptionsLookup $userOptionsLookup,
        WatchlistManager $watchlistManager,
        WikiPageFactory $wikiPageFactory,
        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
        EditFilterHookRunner $editFilterHookRunner,
        EntityDiffVisualizerFactory $entityDiffVisualizerFactory,
        SummaryFormatter $summaryFormatter
    ) {
        parent::__construct(
            $article,
            $context,
            $permissionManager,
            $revisionLookup,
            $anonymousEditWarningBuilder,
            $entityDiffVisualizerFactory,
            $summaryFormatter
        );

        $this->tempUserCreator = $tempUserCreator;
        $this->userOptionsLookup = $userOptionsLookup;
        $this->watchlistManager = $watchlistManager;
        $this->wikiPageFactory = $wikiPageFactory;
        $this->editFilterHookRunner = $editFilterHookRunner;
    }

    public function getName(): string {
        return 'submit';
    }

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

    /**
     * Show the entity using parent::show(), unless an undo operation is requested.
     * In that case $this->undo(); is called to perform the action after a permission check.
     */
    public function show(): void {
        $request = $this->getRequest();

        if ( $request->getCheck( 'undo' ) || $request->getCheck( 'undoafter' ) || $request->getCheck( 'restore' ) ) {
            if ( $this->showPermissionError( 'read' ) || $this->showPermissionError( 'edit' ) ) {
                return;
            }

            $this->undo();
            return;
        }

        parent::show();
    }

    /**
     * Perform the undo operation specified by the web request.
     */
    public function undo(): void {
        $request = $this->getRequest();
        $undidRevId = $request->getInt( 'undo' );
        $undidAfterRevId = $request->getInt( 'undoafter' );
        $restoreId = $request->getInt( 'restore' );
        $title = $this->getTitle();

        if ( !$request->wasPosted() || !$request->getCheck( 'wpSave' ) ) {
            $args = [ 'action' => 'edit' ];

            if ( $undidRevId !== 0 ) {
                $args['undo'] = $undidRevId;
            }

            if ( $undidAfterRevId !== 0 ) {
                $args['undoafter'] = $undidAfterRevId;
            }

            if ( $restoreId !== 0 ) {
                $args['restore'] = $restoreId;
            }

            $undoUrl = $title->getLocalURL( $args );
            $this->getOutput()->redirect( $undoUrl );
            return;
        }

        $revisions = $this->loadRevisions();
        if ( !$revisions->isOK() ) {
            $this->showUndoErrorPage( $revisions );
            return;
        }

        /**
         * @var RevisionRecord $olderRevision
         * @var RevisionRecord $newerRevision
         * @var RevisionRecord $latestRevision
         */
        [ $olderRevision, $newerRevision, $latestRevision ] = $revisions->getValue();
        $patchedContent = $this->getPatchContent( $olderRevision, $newerRevision, $latestRevision );
        if ( !$patchedContent->isOK() ) {
            $this->showUndoErrorPage( $patchedContent );
            return;
        }
        $latestContent = $latestRevision->getContent( SlotRecord::MAIN );

        if ( $patchedContent->getValue()->equals( $latestContent ) ) {
            $status = SubmitEntityStatus::newEdit( null, $this->getContext() );
            $status->warning( 'wikibase-empty-undo' );
        } else {
            $summary = $request->getText( 'wpSummary' );

            if ( $request->getCheck( 'restore' ) ) {
                $summary = $this->makeSummary(
                    'restore',
                    $olderRevision,
                    $summary
                );
            } else {
                $summary = $this->makeSummary(
                    'undo',
                    $newerRevision,
                    $summary
                );
            }

            $editToken = $request->getText( 'wpEditToken' );
            $status = $this->attemptSave( $title, $patchedContent->getValue(), $summary,
                $undidRevId, $undidAfterRevId ?: $restoreId, $editToken );
        }

        if ( $status->isOK() ) {
            $this->redirectToEntityPage( $status );
        } else {
            $this->showUndoErrorPage( $status );
        }
    }

    /**
     * @return Status containing EntityContent
     */
    private function getPatchContent(
        RevisionRecord $olderRevision,
        RevisionRecord $newerRevision,
        RevisionRecord $latestRevision
    ): Status {
        /**
         * @var EntityContent $olderContent
         * @var EntityContent $newerContent
         * @var EntityContent $latestContent
         */
        $olderContent = $olderRevision->getContent( SlotRecord::MAIN );
        $newerContent = $newerRevision->getContent( SlotRecord::MAIN );
        $latestContent = $latestRevision->getContent( SlotRecord::MAIN );
        '@phan-var EntityContent $olderContent';
        '@phan-var EntityContent $newerContent';
        '@phan-var EntityContent $latestContent';

        if ( $newerContent->isRedirect() !== $latestContent->isRedirect() ) {
            return Status::newFatal( $latestContent->isRedirect()
                ? 'wikibase-undo-redirect-latestredirect'
                : 'wikibase-undo-redirect-latestnoredirect' );
        }

        return Status::newGood( $latestContent->getPatchedCopy( $newerContent->getDiff( $olderContent ) ) );
    }

    public function execute(): void {
        // @phan-suppress-previous-line PhanPluginNeverReturnMethod
        throw new LogicException( 'Not applicable.' );
    }

    private function attemptSave(
        Title $title,
        EntityContent $content,
        string $summary,
        int $undidRevId,
        int $originalRevId,
        string $editToken
    ): SubmitEntityStatus {
        $status = $this->getEditTokenStatus( $editToken );
        if ( !$status->isOK() ) {
            return SubmitEntityStatus::wrap( $status );
        }

        $status = $this->getTempUserStatus();
        if ( !$status->isOK() ) {
            return $status;
        }

        $status->merge( $this->permissionManager->getPermissionStatus(
            'edit', $this->getUser(), $title, PermissionManager::RIGOR_SECURE ) );

        if ( !$status->isOK() ) {
            return $status;
        }

        $status->merge( $this->editFilterHookRunner->run( $content, $status->getContext(), $summary ) );

        if ( !$status->isOK() ) {
            return $status;
        }

        // save edit
        $page = $this->wikiPageFactory->newFromTitle( $title );

        // NOTE: Constraint checks are performed automatically via EntityHandler::validateSave.
        $status->merge( $page->doUserEditContent(
            $content,
            $status->getSavedTempUser() ?? $this->getUser(),
            $summary,
            /* flags */ 0,
            $originalRevId ?: false,
            /* tags */ [],
            $undidRevId
        ) );

        if ( !$status->isOK() ) {
            return $status;
        }

        $this->doWatch( $title );

        return $status;
    }

    /**
     * Checks that the given token is valid.
     */
    private function getEditTokenStatus( string $editToken ): Status {
        $status = Status::newGood();
        $user = $this->getUser();
        if ( !$user->matchEditToken( $editToken ) ) {
            $status = Status::newFatal( 'session_fail_preview' );
        }
        return $status;
    }

    private function getTempUserStatus(): SubmitEntityStatus {
        if ( $this->tempUserCreator->shouldAutoCreate( $this->getUser(), 'edit' ) ) {
            $status = $this->tempUserCreator->create( null, $this->getRequest() );
            if ( !$status->isOK() ) {
                return SubmitEntityStatus::wrap( $status );
            }
            $user = $status->getUser();
            $context = new DerivativeContext( $this->getContext() );
            $context->setUser( $user );
            return SubmitEntityStatus::newEdit( $user, $context );
        } else {
            return SubmitEntityStatus::newEdit( null, $this->getContext() );
        }
    }

    /**
     * Update watchlist.
     */
    private function doWatch( Title $title ): void {
        $user = $this->getUser();

        if ( $user->isNamed()
            && $this->userOptionsLookup->getOption( $user, 'watchdefault' )
            && !$this->watchlistManager->isWatchedIgnoringRights( $user, $title )
        ) {
            $this->watchlistManager->addWatchIgnoringRights( $user, $title );
        }
    }

    /**
     * Redirect to the page of the entity that was successfully edited.
     *
     * @param SubmitEntityStatus $status A status as returned by {@link self::attemptSave()}.
     * The status must be {@link StatusValue::isOK() OK}.
     */
    private function redirectToEntityPage( SubmitEntityStatus $status ): void {
        Assert::parameter( $status->isOK(), '$status', 'must be OK' );
        $title = $this->getTitle();
        $savedTempUser = $status->getSavedTempUser();
        $redirectUrl = '';
        if ( $savedTempUser !== null ) {
            $this->getHookRunner()->onTempUserCreatedRedirect(
                $this->getRequest()->getSession(),
                $savedTempUser,
                $title->getPrefixedDBkey(),
                '',
                '',
                $redirectUrl
            );
        }
        if ( !$redirectUrl ) {
            $redirectUrl = $title->getFullURL();
        }
        $this->getOutput()->redirect( $redirectUrl );
    }

}