Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.97% covered (success)
90.97%
131 / 144
68.75% covered (warning)
68.75%
11 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMergeLexemes
90.97% covered (success)
90.97%
131 / 144
68.75% covered (warning)
68.75%
11 / 16
33.80
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
 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\Html\Html;
10use MediaWiki\HTMLForm\HTMLForm;
11use MediaWiki\Message\Message;
12use MediaWiki\Permissions\PermissionManager;
13use MediaWiki\SpecialPage\SpecialPage;
14use UserBlockedError;
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', 'item-merge' );
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 execute( $subPage ): void {
72        $this->setHeaders();
73        $this->outputHeader( 'wikibase-mergelexemes-summary' );
74
75        if ( !$this->userCanExecute( $this->getUser() ) ) {
76            $this->displayRestrictionError();
77        }
78        $this->checkBlocked();
79
80        $sourceId = $this->getTextParam( self::FROM_ID );
81        $targetId = $this->getTextParam( self::TO_ID );
82
83        if ( $sourceId && $targetId ) {
84            $sourceLexemeId = $this->getLexemeId( $sourceId );
85            $targetLexemeId = $this->getLexemeId( $targetId );
86            if ( $sourceLexemeId && $targetLexemeId ) {
87                if ( $this->getRequest()->getBool( self::SUCCESS ) ) {
88                    // redirected back here after a successful edit + temp user, show success now
89                    // (the success may be inaccurate if users created this URL manually, but that’s harmless)
90                    $this->showSuccessMessage( $sourceLexemeId, $targetLexemeId );
91                } else {
92                    $this->mergeLexemes( $sourceLexemeId, $targetLexemeId );
93                }
94            } else {
95                if ( !$sourceLexemeId ) {
96                    $this->showInvalidLexemeIdError( $sourceId );
97                }
98                if ( !$targetLexemeId ) {
99                    $this->showInvalidLexemeIdError( $targetId );
100                }
101            }
102        }
103
104        $this->showMergeForm();
105    }
106
107    public function setHeaders(): void {
108        $out = $this->getOutput();
109        $out->setArticleRelated( false );
110        $out->setPageTitleMsg( $this->getDescription() );
111    }
112
113    private function checkBlocked(): void {
114        $checkReplica = !$this->getRequest()->wasPosted();
115        $userBlock = $this->getUser()->getBlock( $checkReplica );
116        if (
117            $userBlock !== null &&
118            $this->permissionManager->isBlockedFrom(
119                $this->getUser(),
120                $this->getFullTitle(),
121                $checkReplica
122            )
123        ) {
124            throw new UserBlockedError( $userBlock );
125        }
126    }
127
128    public static function factory(
129        PermissionManager $permissionManager,
130        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
131        EntityTitleLookup $entityTitleLookup,
132        ExceptionLocalizer $exceptionLocalizer,
133        SettingsArray $repoSettings,
134        TokenCheckInteractor $tokenCheckInteractor,
135        MergeLexemesInteractor $mergeLexemesInteractor
136    ): self {
137        return new self(
138            $repoSettings->getSetting( 'specialPageTags' ),
139            $mergeLexemesInteractor,
140            $tokenCheckInteractor,
141            $entityTitleLookup,
142            $exceptionLocalizer,
143            $permissionManager,
144            $anonymousEditWarningBuilder
145        );
146    }
147
148    private function showMergeForm(): void {
149        HTMLForm::factory( 'ooui', $this->getFormElements(), $this->getContext() )
150            ->setId( 'wb-mergelexemes' )
151            ->setPreHtml( $this->anonymousEditWarning() )
152            ->setHeaderHtml( $this->msg( 'wikibase-lexeme-mergelexemes-intro' )->parse() )
153            ->setSubmitID( 'wb-mergelexemes-submit' )
154            ->setSubmitName( 'wikibase-lexeme-mergelexemes-submit' )
155            ->setSubmitTextMsg( 'wikibase-lexeme-mergelexemes-submit' )
156            ->setWrapperLegendMsg( 'special-mergelexemes' )
157            ->setSubmitCallback( static function () {
158            } )
159            ->show();
160    }
161
162    private function getFormElements(): array {
163        return [
164            self::FROM_ID => [
165                'name' => self::FROM_ID,
166                'default' => $this->getRequest()->getVal( self::FROM_ID ),
167                'type' => 'text',
168                'id' => 'wb-mergelexemes-from-id',
169                'label-message' => 'wikibase-lexeme-mergelexemes-from-id',
170            ],
171            self::TO_ID => [
172                'name' => self::TO_ID,
173                'default' => $this->getRequest()->getVal( self::TO_ID ),
174                'type' => 'text',
175                'id' => 'wb-mergelexemes-to-id',
176                'label-message' => 'wikibase-lexeme-mergelexemes-to-id',
177            ],
178        ];
179    }
180
181    private function anonymousEditWarning(): string {
182        if ( !$this->getUser()->isRegistered() ) {
183            $fullTitle = $this->getPageTitle();
184            return Html::rawElement(
185                'p',
186                [ 'class' => 'warning' ],
187                $this->anonymousEditWarningBuilder->buildAnonymousEditWarningHTML( $fullTitle->getPrefixedText() )
188            );
189        }
190
191        return '';
192    }
193
194    private function mergeLexemes( LexemeId $sourceId, LexemeId $targetId ): void {
195        try {
196            $this->tokenCheckInteractor->checkRequestToken( $this->getContext(), 'wpEditToken' );
197        } catch ( TokenCheckException $e ) {
198            $message = $this->exceptionLocalizer->getExceptionMessage( $e );
199            $this->showErrorHTML( $message->parse() );
200            return;
201        }
202
203        try {
204            $status = $this->mergeInteractor->mergeLexemes(
205                $sourceId,
206                $targetId,
207                $this->getContext(),
208                null,
209                false,
210                $this->tags
211            );
212            $savedTempUser = $status->getSavedTempUser();
213        } catch ( MergingException $e ) {
214            $this->showErrorHTML( $e->getErrorMessage()->escaped() );
215            return;
216        }
217
218        if ( $savedTempUser !== null ) {
219            $redirectUrl = '';
220            $this->getHookRunner()->onTempUserCreatedRedirect(
221                $this->getRequest()->getSession(),
222                $savedTempUser,
223                $this->getPageTitle()->getPrefixedDBkey(),
224                wfArrayToCgi( [
225                    self::FROM_ID => $sourceId->getSerialization(),
226                    self::TO_ID => $targetId->getSerialization(),
227                    self::SUCCESS => '1',
228                ] ),
229                '',
230                $redirectUrl
231            );
232            if ( $redirectUrl ) {
233                $this->getOutput()->redirect( $redirectUrl );
234                return; // success will be shown when returning here from redirect
235            }
236        }
237
238        $this->showSuccessMessage( $sourceId, $targetId );
239    }
240
241    private function getTextParam( string $name ): string {
242        $value = $this->getRequest()->getText( $name, '' );
243        return trim( $value );
244    }
245
246    /**
247     * @param string $idSerialization
248     *
249     * @return LexemeId|false
250     */
251    private function getLexemeId( string $idSerialization ) {
252        try {
253            return new LexemeId( $idSerialization );
254        } catch ( InvalidArgumentException $e ) {
255            return false;
256        }
257    }
258
259    private function showSuccessMessage( LexemeId $sourceId, LexemeId $targetId ): void {
260        try {
261            $sourceTitle = $this->titleLookup->getTitleForId( $sourceId );
262            $targetTitle = $this->titleLookup->getTitleForId( $targetId );
263        } catch ( Exception $e ) {
264            $this->showErrorHTML( $this->exceptionLocalizer->getExceptionMessage( $e )->escaped() );
265            return;
266        }
267
268        $this->getOutput()->addWikiMsg(
269            'wikibase-lexeme-mergelexemes-success',
270            Message::rawParam(
271                $this->getLinkRenderer()->makePreloadedLink( $sourceTitle )
272            ),
273            Message::rawParam(
274                $this->getLinkRenderer()->makePreloadedLink( $targetTitle )
275            )
276        );
277    }
278
279    private function showInvalidLexemeIdError( $id ): void {
280        $this->showErrorHTML(
281            ( new Message( 'wikibase-lexeme-mergelexemes-error-invalid-id', [ $id ] ) )
282                ->escaped()
283        );
284    }
285
286    protected function getGroupName(): string {
287        return 'wikibase';
288    }
289
290    protected function showErrorHTML( $error ): void {
291        $this->getOutput()->addHTML( '<p class="error">' . $error . '</p>' );
292    }
293
294    public function getDescription(): Message {
295        return $this->msg( 'special-mergelexemes' );
296    }
297
298}