Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.85% covered (success)
98.85%
86 / 87
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
MergeLexemesInteractor
98.85% covered (success)
98.85%
86 / 87
88.89% covered (warning)
88.89%
8 / 9
16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 mergeLexemes
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 checkCanMerge
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getLexeme
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 validateEntities
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getSummary
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 attemptSaveMerge
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
 saveLexeme
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 updateWatchlistEntries
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare( strict_types = 1 );
4
5namespace Wikibase\Lexeme\Interactors\MergeLexemes;
6
7use IContextSource;
8use MediaWiki\Permissions\PermissionManager;
9use WatchedItemStoreInterface;
10use Wikibase\DataModel\Entity\EntityDocument;
11use Wikibase\Lexeme\DataAccess\Store\MediaWikiLexemeRedirector;
12use Wikibase\Lexeme\Domain\Merge\Exceptions\LexemeLoadingException;
13use Wikibase\Lexeme\Domain\Merge\Exceptions\LexemeNotFoundException;
14use Wikibase\Lexeme\Domain\Merge\Exceptions\LexemeSaveFailedException;
15use Wikibase\Lexeme\Domain\Merge\Exceptions\MergingException;
16use Wikibase\Lexeme\Domain\Merge\Exceptions\PermissionDeniedException;
17use Wikibase\Lexeme\Domain\Merge\Exceptions\ReferenceSameLexemeException;
18use Wikibase\Lexeme\Domain\Merge\LexemeMerger;
19use Wikibase\Lexeme\Domain\Model\Lexeme;
20use Wikibase\Lexeme\Domain\Model\LexemeId;
21use Wikibase\Lib\FormatableSummary;
22use Wikibase\Lib\Store\EntityRevisionLookup;
23use Wikibase\Lib\Store\LookupConstants;
24use Wikibase\Lib\Store\RevisionedUnresolvedRedirectException;
25use Wikibase\Lib\Store\StorageException;
26use Wikibase\Lib\Summary;
27use Wikibase\Repo\Content\EntityContent;
28use Wikibase\Repo\EditEntity\EditEntityStatus;
29use Wikibase\Repo\EditEntity\MediaWikiEditEntityFactory;
30use Wikibase\Repo\Store\EntityPermissionChecker;
31use Wikibase\Repo\Store\EntityTitleStoreLookup;
32use Wikibase\Repo\SummaryFormatter;
33
34/**
35 * @license GPL-2.0-or-later
36 */
37class MergeLexemesInteractor {
38
39    private SummaryFormatter $summaryFormatter;
40    private EntityRevisionLookup $entityRevisionLookup;
41    private MediaWikiLexemeRedirector $lexemeRedirector;
42    private EntityPermissionChecker $permissionChecker;
43    private PermissionManager $permissionManager;
44    private EntityTitleStoreLookup $entityTitleLookup;
45    private LexemeMerger $lexemeMerger;
46    private WatchedItemStoreInterface $watchedItemStore;
47    private MediaWikiEditEntityFactory $editEntityFactory;
48
49    public function __construct(
50        LexemeMerger $lexemeMerger,
51        SummaryFormatter $summaryFormatter,
52        MediaWikiLexemeRedirector $lexemeRedirector,
53        EntityPermissionChecker $permissionChecker,
54        PermissionManager $permissionManager,
55        EntityTitleStoreLookup $entityTitleLookup,
56        WatchedItemStoreInterface $watchedItemStore,
57        EntityRevisionLookup $entityRevisionLookup,
58        MediaWikiEditEntityFactory $editEntityFactory
59    ) {
60        $this->lexemeMerger = $lexemeMerger;
61        $this->summaryFormatter = $summaryFormatter;
62        $this->lexemeRedirector = $lexemeRedirector;
63        $this->permissionChecker = $permissionChecker;
64        $this->permissionManager = $permissionManager;
65        $this->entityTitleLookup = $entityTitleLookup;
66        $this->watchedItemStore = $watchedItemStore;
67        $this->entityRevisionLookup = $entityRevisionLookup;
68        $this->editEntityFactory = $editEntityFactory;
69    }
70
71    /**
72     * @param LexemeId $sourceId
73     * @param LexemeId $targetId
74     * @param string|null $summary - only relevant when called through the API
75     * @param string[] $tags
76     *
77     * @return MergeLexemesStatus Note that the status is only returned
78     * to wrap the context and saved temp user in a strongly typed container.
79     * Errors are (currently) reported as exceptions, not as a failed status.
80     * (It would be nice to fix this at some point and use status consistently.)
81     *
82     * @throws MergingException
83     */
84    public function mergeLexemes(
85        LexemeId $sourceId,
86        LexemeId $targetId,
87        IContextSource $context,
88        ?string $summary = null,
89        bool $botEditRequested = false,
90        array $tags = []
91    ): MergeLexemesStatus {
92        $this->checkCanMerge( $sourceId, $context );
93        $this->checkCanMerge( $targetId, $context );
94
95        $source = $this->getLexeme( $sourceId );
96        $target = $this->getLexeme( $targetId );
97
98        $this->validateEntities( $source, $target );
99
100        $this->lexemeMerger->merge( $source, $target );
101
102        $mergeStatus = $this->attemptSaveMerge( $source, $target, $context, $summary, $botEditRequested, $tags );
103        $context = $mergeStatus->getContext();
104        $this->updateWatchlistEntries( $sourceId, $targetId );
105
106        $redirectStatus = $this->lexemeRedirector
107            ->createRedirect( $sourceId, $targetId, $botEditRequested, $tags, $context );
108        $context = $redirectStatus->getContext();
109
110        return MergeLexemesStatus::newMerge(
111            $mergeStatus->getSavedTempUser() ?? $redirectStatus->getSavedTempUser(),
112            $context
113        );
114    }
115
116    private function checkCanMerge( LexemeId $lexemeId, IContextSource $context ): void {
117        $status = $this->permissionChecker->getPermissionStatusForEntityId(
118            $context->getUser(),
119            EntityPermissionChecker::ACTION_MERGE,
120            $lexemeId
121        );
122
123        if ( !$status->isOK() ) {
124            // would be nice to propagate the errors from $status...
125            throw new PermissionDeniedException();
126        }
127    }
128
129    /**
130     * @throws MergingException
131     */
132    private function getLexeme( LexemeId $lexemeId ): Lexeme {
133        try {
134            $revision = $this->entityRevisionLookup->getEntityRevision(
135                $lexemeId,
136                0,
137                LookupConstants::LATEST_FROM_MASTER
138            );
139
140            if ( $revision ) {
141                // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
142                return $revision->getEntity();
143            } else {
144                throw new LexemeNotFoundException( $lexemeId );
145            }
146        } catch ( StorageException | RevisionedUnresolvedRedirectException $ex ) {
147            throw new LexemeLoadingException();
148        }
149    }
150
151    /**
152     * @throws ReferenceSameLexemeException
153     */
154    private function validateEntities( EntityDocument $fromEntity, EntityDocument $toEntity ): void {
155        if ( $toEntity->getId()->equals( $fromEntity->getId() ) ) {
156            throw new ReferenceSameLexemeException();
157        }
158    }
159
160    /**
161     * @param string $direction either 'from' or 'to'
162     * @param LexemeId $id
163     * @param string|null $customSummary
164     *
165     * @return Summary
166     */
167    private function getSummary(
168        string $direction,
169        LexemeId $id,
170        ?string $customSummary = null
171    ): Summary {
172        $summary = new Summary( 'wblmergelexemes', $direction, null, [ $id->getSerialization() ] );
173        $summary->setUserSummary( $customSummary );
174
175        return $summary;
176    }
177
178    private function attemptSaveMerge(
179        Lexeme $source,
180        Lexeme $target,
181        IContextSource $context,
182        ?string $summary,
183        bool $botEditRequested,
184        array $tags
185    ): MergeLexemesStatus {
186        $toResult = $this->saveLexeme(
187            $source,
188            $context,
189            $this->getSummary( 'to', $target->getId(), $summary ),
190            $botEditRequested,
191            $tags
192        );
193        $context = $toResult->getContext();
194
195        $fromResult = $this->saveLexeme(
196            $target,
197            $context,
198            $this->getSummary( 'from', $source->getId(), $summary ),
199            $botEditRequested,
200            $tags
201        );
202        $context = $fromResult->getContext();
203
204        return MergeLexemesStatus::newMerge(
205            $fromResult->getSavedTempUser() ?? $toResult->getSavedTempUser(),
206            $context
207        );
208    }
209
210    private function saveLexeme(
211        Lexeme $lexeme,
212        IContextSource $context,
213        FormatableSummary $summary,
214        bool $botEditRequested,
215        array $tags
216    ): EditEntityStatus {
217        // TODO: the EntityContent::EDIT_IGNORE_CONSTRAINTS flag does not seem to be used by Lexeme
218        // (LexemeHandler has no onSaveValidators)
219        $flags = EDIT_UPDATE | EntityContent::EDIT_IGNORE_CONSTRAINTS;
220        if ( $botEditRequested && $this->permissionManager->userHasRight( $context->getUser(), 'bot' ) ) {
221            $flags |= EDIT_FORCE_BOT;
222        }
223
224        $formattedSummary = $this->summaryFormatter->formatSummary( $summary );
225
226        $editEntity = $this->editEntityFactory->newEditEntity( $context, $lexeme->getId() );
227        $status = $editEntity->attemptSave(
228            $lexeme,
229            $formattedSummary,
230            $flags,
231            false,
232            null,
233            $tags
234        );
235        if ( !$status->isOK() ) {
236            throw new LexemeSaveFailedException( $status->getWikiText() );
237        }
238
239        return $status;
240    }
241
242    private function updateWatchlistEntries( LexemeId $fromId, LexemeId $toId ): void {
243        $this->watchedItemStore->duplicateAllAssociatedEntries(
244            $this->entityTitleLookup->getTitleForId( $fromId ),
245            $this->entityTitleLookup->getTitleForId( $toId )
246        );
247    }
248
249}