Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.03% covered (success)
91.03%
132 / 145
70.59% covered (warning)
70.59%
12 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMergeLexemes
91.03% covered (success)
91.03%
132 / 145
70.59% covered (warning)
70.59%
12 / 17
34.83
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getRestriction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
9.01
 setHeaders
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 checkBlocked
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 factory
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 showMergeForm
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getFormElements
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 anonymousEditWarning
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 mergeLexemes
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
5
 getTextParam
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLexemeId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 showSuccessMessage
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 showInvalidLexemeIdError
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 showErrorHTML
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDescription
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare( strict_types = 1 );
4
5namespace Wikibase\Lexeme\MediaWiki\Specials;
6
7use Exception;
8use InvalidArgumentException;
9use MediaWiki\Exception\UserBlockedError;
10use MediaWiki\Html\Html;
11use MediaWiki\HTMLForm\HTMLForm;
12use MediaWiki\Message\Message;
13use MediaWiki\Permissions\PermissionManager;
14use MediaWiki\SpecialPage\SpecialPage;
15use Wikibase\Lexeme\Domain\Merge\Exceptions\MergingException;
16use Wikibase\Lexeme\Domain\Model\LexemeId;
17use Wikibase\Lexeme\Interactors\MergeLexemes\MergeLexemesInteractor;
18use Wikibase\Lib\SettingsArray;
19use Wikibase\Lib\Store\EntityTitleLookup;
20use Wikibase\Repo\AnonymousEditWarningBuilder;
21use Wikibase\Repo\Interactors\TokenCheckException;
22use Wikibase\Repo\Interactors\TokenCheckInteractor;
23use Wikibase\Repo\Localizer\ExceptionLocalizer;
24
25/**
26 * Special page for merging one lexeme into another.
27 *
28 * @license GPL-2.0-or-later
29 */
30class SpecialMergeLexemes extends SpecialPage {
31
32    private const FROM_ID = 'from-id';
33    private const TO_ID = 'to-id';
34    private const SUCCESS = 'success';
35
36    /** @var string[] */
37    private array $tags;
38
39    private MergeLexemesInteractor $mergeInteractor;
40
41    private TokenCheckInteractor $tokenCheckInteractor;
42
43    private EntityTitleLookup $titleLookup;
44
45    private ExceptionLocalizer $exceptionLocalizer;
46
47    private PermissionManager $permissionManager;
48
49    private AnonymousEditWarningBuilder $anonymousEditWarningBuilder;
50
51    public function __construct(
52        array $tags,
53        MergeLexemesInteractor $mergeInteractor,
54        TokenCheckInteractor $tokenCheckInteractor,
55        EntityTitleLookup $titleLookup,
56        ExceptionLocalizer $exceptionLocalizer,
57        PermissionManager $permissionManager,
58        AnonymousEditWarningBuilder $anonymousEditWarningBuilder
59    ) {
60        parent::__construct( 'MergeLexemes' );
61        $this->tags = $tags;
62        $this->mergeInteractor = $mergeInteractor;
63        $this->tokenCheckInteractor = $tokenCheckInteractor;
64        $this->titleLookup = $titleLookup;
65        $this->exceptionLocalizer = $exceptionLocalizer;
66        $this->permissionManager = $permissionManager;
67        $this->anonymousEditWarningBuilder = $anonymousEditWarningBuilder;
68    }
69
70    /** @inheritDoc */
71    public function getRestriction(): string {
72        return 'item-merge';
73    }
74
75    /** @inheritDoc */
76    public function execute( $subPage ): void {
77        $this->setHeaders();
78        $this->outputHeader( 'wikibase-mergelexemes-summary' );
79
80        if ( !$this->userCanExecute( $this->getUser() ) ) {
81            $this->displayRestrictionError();
82        }
83        $this->checkBlocked();
84
85        $sourceId = $this->getTextParam( self::FROM_ID );
86        $targetId = $this->getTextParam( self::TO_ID );
87
88        if ( $sourceId && $targetId ) {
89            $sourceLexemeId = $this->getLexemeId( $sourceId );
90            $targetLexemeId = $this->getLexemeId( $targetId );
91            if ( $sourceLexemeId && $targetLexemeId ) {
92                if ( $this->getRequest()->getBool( self::SUCCESS ) ) {
93                    // redirected back here after a successful edit + temp user, show success now
94                    // (the success may be inaccurate if users created this URL manually, but that’s harmless)
95                    $this->showSuccessMessage( $sourceLexemeId, $targetLexemeId );
96                } else {
97                    $this->mergeLexemes( $sourceLexemeId, $targetLexemeId );
98                }
99            } else {
100                if ( !$sourceLexemeId ) {
101                    $this->showInvalidLexemeIdError( $sourceId );
102                }
103                if ( !$targetLexemeId ) {
104                    $this->showInvalidLexemeIdError( $targetId );
105                }
106            }
107        }
108
109        $this->showMergeForm();
110    }
111
112    public function setHeaders(): void {
113        $out = $this->getOutput();
114        $out->setArticleRelated( false );
115        $out->setPageTitleMsg( $this->getDescription() );
116    }
117
118    private function checkBlocked(): void {
119        $checkReplica = !$this->getRequest()->wasPosted();
120        $userBlock = $this->getUser()->getBlock( $checkReplica );
121        if (
122            $userBlock !== null &&
123            $this->permissionManager->isBlockedFrom(
124                $this->getUser(),
125                $this->getFullTitle(),
126                $checkReplica
127            )
128        ) {
129            throw new UserBlockedError( $userBlock );
130        }
131    }
132
133    public static function factory(
134        PermissionManager $permissionManager,
135        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
136        EntityTitleLookup $entityTitleLookup,
137        ExceptionLocalizer $exceptionLocalizer,
138        SettingsArray $repoSettings,
139        TokenCheckInteractor $tokenCheckInteractor,
140        MergeLexemesInteractor $mergeLexemesInteractor
141    ): self {
142        return new self(
143            $repoSettings->getSetting( 'specialPageTags' ),
144            $mergeLexemesInteractor,
145            $tokenCheckInteractor,
146            $entityTitleLookup,
147            $exceptionLocalizer,
148            $permissionManager,
149            $anonymousEditWarningBuilder
150        );
151    }
152
153    private function showMergeForm(): void {
154        HTMLForm::factory( 'ooui', $this->getFormElements(), $this->getContext() )
155            ->setId( 'wb-mergelexemes' )
156            ->setPreHtml( $this->anonymousEditWarning() )
157            ->setHeaderHtml( $this->msg( 'wikibase-lexeme-mergelexemes-intro' )->parse() )
158            ->setSubmitID( 'wb-mergelexemes-submit' )
159            ->setSubmitName( 'wikibase-lexeme-mergelexemes-submit' )
160            ->setSubmitTextMsg( 'wikibase-lexeme-mergelexemes-submit' )
161            ->setWrapperLegendMsg( 'special-mergelexemes' )
162            ->setSubmitCallback( static function () {
163            } )
164            ->show();
165    }
166
167    private function getFormElements(): array {
168        return [
169            self::FROM_ID => [
170                'name' => self::FROM_ID,
171                'default' => $this->getRequest()->getVal( self::FROM_ID ),
172                'type' => 'text',
173                'id' => 'wb-mergelexemes-from-id',
174                'label-message' => 'wikibase-lexeme-mergelexemes-from-id',
175            ],
176            self::TO_ID => [
177                'name' => self::TO_ID,
178                'default' => $this->getRequest()->getVal( self::TO_ID ),
179                'type' => 'text',
180                'id' => 'wb-mergelexemes-to-id',
181                'label-message' => 'wikibase-lexeme-mergelexemes-to-id',
182            ],
183        ];
184    }
185
186    private function anonymousEditWarning(): string {
187        if ( !$this->getUser()->isRegistered() ) {
188            $fullTitle = $this->getPageTitle();
189            $this->getOutput()->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
190            return Html::warningBox(
191                $this->msg( $this->anonymousEditWarningBuilder->buildAnonymousEditWarningMessage( $fullTitle ) )
192                    ->parse()
193            );
194        }
195
196        return '';
197    }
198
199    private function mergeLexemes( LexemeId $sourceId, LexemeId $targetId ): void {
200        try {
201            $this->tokenCheckInteractor->checkRequestToken( $this->getContext(), 'wpEditToken' );
202        } catch ( TokenCheckException $e ) {
203            $message = $this->exceptionLocalizer->getExceptionMessage( $e );
204            $this->showErrorHTML( $message->parse() );
205            return;
206        }
207
208        try {
209            $status = $this->mergeInteractor->mergeLexemes(
210                $sourceId,
211                $targetId,
212                $this->getContext(),
213                null,
214                false,
215                $this->tags
216            );
217            $savedTempUser = $status->getSavedTempUser();
218        } catch ( MergingException $e ) {
219            $this->showErrorHTML( $e->getErrorMessage()->escaped() );
220            return;
221        }
222
223        if ( $savedTempUser !== null ) {
224            $redirectUrl = '';
225            $this->getHookRunner()->onTempUserCreatedRedirect(
226                $this->getRequest()->getSession(),
227                $savedTempUser,
228                $this->getPageTitle()->getPrefixedDBkey(),
229                wfArrayToCgi( [
230                    self::FROM_ID => $sourceId->getSerialization(),
231                    self::TO_ID => $targetId->getSerialization(),
232                    self::SUCCESS => '1',
233                ] ),
234                '',
235                $redirectUrl
236            );
237            if ( $redirectUrl ) {
238                $this->getOutput()->redirect( $redirectUrl );
239                return; // success will be shown when returning here from redirect
240            }
241        }
242
243        $this->showSuccessMessage( $sourceId, $targetId );
244    }
245
246    private function getTextParam( string $name ): string {
247        $value = $this->getRequest()->getText( $name, '' );
248        return trim( $value );
249    }
250
251    /**
252     * @param string $idSerialization
253     *
254     * @return LexemeId|false
255     */
256    private function getLexemeId( string $idSerialization ) {
257        try {
258            return new LexemeId( $idSerialization );
259        } catch ( InvalidArgumentException ) {
260            return false;
261        }
262    }
263
264    private function showSuccessMessage( LexemeId $sourceId, LexemeId $targetId ): void {
265        try {
266            $sourceTitle = $this->titleLookup->getTitleForId( $sourceId );
267            $targetTitle = $this->titleLookup->getTitleForId( $targetId );
268        } catch ( Exception $e ) {
269            $this->showErrorHTML( $this->exceptionLocalizer->getExceptionMessage( $e )->escaped() );
270            return;
271        }
272
273        $this->getOutput()->addWikiMsg(
274            'wikibase-lexeme-mergelexemes-success',
275            Message::rawParam(
276                $this->getLinkRenderer()->makePreloadedLink( $sourceTitle )
277            ),
278            Message::rawParam(
279                $this->getLinkRenderer()->makePreloadedLink( $targetTitle )
280            )
281        );
282    }
283
284    private function showInvalidLexemeIdError( string $id ): void {
285        $this->showErrorHTML(
286            ( new Message( 'wikibase-lexeme-mergelexemes-error-invalid-id', [ $id ] ) )
287                ->escaped()
288        );
289    }
290
291    protected function getGroupName(): string {
292        return 'wikibase';
293    }
294
295    protected function showErrorHTML( string $error ): void {
296        $this->getOutput()->addHTML( '<p class="error">' . $error . '</p>' );
297    }
298
299    public function getDescription(): Message {
300        return $this->msg( 'special-mergelexemes' );
301    }
302
303}