Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 189
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeleteTranslatableBundleSpecialPage
0.00% covered (danger)
0.00%
0 / 189
0.00% covered (danger)
0.00%
0 / 14
1892
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
240
 doBasicChecks
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 checkToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 showForm
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 showConfirmation
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
42
 getChangeLine
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 performAction
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getCommonFormFields
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
2
 listPages
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getDeleteReason
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 identifyEntityType
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 isTranslation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\PageTranslation;
5
6use ErrorPageError;
7use HTMLForm;
8use MediaWiki\Extension\Translate\MessageBundleTranslation\MessageBundle;
9use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundleFactory;
10use MediaWiki\Extension\Translate\Utilities\Utilities;
11use MediaWiki\Permissions\PermissionManager;
12use MediaWiki\Title\Title;
13use OutputPage;
14use PermissionsError;
15use ReadOnlyError;
16use UnlistedSpecialPage;
17use WebRequest;
18use Xml;
19
20/**
21 * Special page which enables deleting translations of translatable bundles and translation pages
22 * @author Niklas Laxström
23 * @license GPL-2.0-or-later
24 * @ingroup SpecialPage PageTranslation
25 */
26class DeleteTranslatableBundleSpecialPage extends UnlistedSpecialPage {
27    // Basic form parameters both as text and as titles
28    private string $text;
29    private ?Title $title;
30    // Other form parameters
31    /** There must be reason for everything. */
32    private string $reason;
33    /** Allow skipping non-translation subpages. */
34    private bool $doSubpages = false;
35    /** Contains the language code if we are working with translation page */
36    private ?string $code;
37    private PermissionManager $permissionManager;
38    private TranslatableBundleDeleter $bundleDeleter;
39    private TranslatableBundleFactory $bundleFactory;
40    private string $entityType;
41    private const PAGE_TITLE_MSG = [
42        'messagebundle' => 'pt-deletepage-mb-title',
43        'translatablepage' => 'pt-deletepage-tp-title',
44        'translationpage' => 'pt-deletepage-lang-title'
45    ];
46    private const WRAPPER_LEGEND_MSG = [
47        'messagebundle' => 'pt-deletepage-mb-legend',
48        'translatablepage' => 'pt-deletepage-tp-title',
49        'translationpage' => 'pt-deletepage-tp-legend'
50    ];
51    private const LOG_PAGE = [
52        'messagebundle' => 'Special:Log/messagebundle',
53        'translatablepage' => 'Special:Log/pagetranslation',
54        'translationpage' => 'Special:Log/pagetranslation'
55    ];
56
57    public function __construct(
58        PermissionManager $permissionManager,
59        TranslatableBundleDeleter $bundleDeleter,
60        TranslatableBundleFactory $bundleFactory
61    ) {
62        parent::__construct( 'PageTranslationDeletePage', 'pagetranslation' );
63        $this->permissionManager = $permissionManager;
64        $this->bundleFactory = $bundleFactory;
65        $this->bundleDeleter = $bundleDeleter;
66    }
67
68    public function doesWrites() {
69        return true;
70    }
71
72    public function execute( $par ) {
73        $this->addHelpLink( 'Help:Deletion_and_undeletion' );
74
75        $request = $this->getRequest();
76
77        $par = (string)$par;
78
79        // Yes, the use of getVal() and getText() is wanted, see bug T22365
80        $this->text = $request->getVal( 'wpTitle', $par );
81        $this->title = Title::newFromText( $this->text );
82        $this->reason = $this->getDeleteReason( $request );
83        $this->doSubpages = $request->getBool( 'subpages' );
84
85        if ( !$this->doBasicChecks() ) {
86            return;
87        }
88
89        $out = $this->getOutput();
90
91        // Real stuff starts here
92        $entityType = $this->identifyEntityType();
93        if ( !$entityType ) {
94            throw new ErrorPageError( 'pt-deletepage-invalid-title', 'pt-deletepage-invalid-text' );
95        }
96        $this->entityType = $entityType;
97
98        if ( $this->isTranslation() ) {
99            [ , $this->code ] = Utilities::figureMessage( $this->title->getText() );
100        } else {
101            $this->code = null;
102        }
103
104        $out->setPageTitle(
105            $this->msg( self::PAGE_TITLE_MSG[ $this->entityType ], $this->title->getPrefixedText() )
106        );
107
108        if ( !$this->getUser()->isAllowed( 'pagetranslation' ) ) {
109            throw new PermissionsError( 'pagetranslation' );
110        }
111
112        // Is there really no better way to do this?
113        $subactionText = $request->getText( 'subaction' );
114        switch ( $subactionText ) {
115            case $this->msg( 'pt-deletepage-action-check' )->text():
116                $subaction = 'check';
117                break;
118            case $this->msg( 'pt-deletepage-action-perform' )->text():
119                $subaction = 'perform';
120                break;
121            case $this->msg( 'pt-deletepage-action-other' )->text():
122                $subaction = '';
123                break;
124            default:
125                $subaction = '';
126        }
127
128        if ( $subaction === 'check' && $this->checkToken() && $request->wasPosted() ) {
129            $this->showConfirmation();
130        } elseif ( $subaction === 'perform' && $this->checkToken() && $request->wasPosted() ) {
131            $this->performAction();
132        } else {
133            $this->showForm();
134        }
135    }
136
137    /**
138     * Do the basic checks whether moving is possible and whether
139     * the input looks anywhere near sane.
140     * @throws PermissionsError|ErrorPageError|ReadOnlyError
141     */
142    private function doBasicChecks(): bool {
143        // Check rights
144        if ( !$this->userCanExecute( $this->getUser() ) ) {
145            $this->displayRestrictionError();
146        }
147
148        if ( $this->title === null ) {
149            throw new ErrorPageError( 'notargettitle', 'notargettext' );
150        }
151
152        if ( !$this->title->exists() ) {
153            throw new ErrorPageError( 'nopagetitle', 'nopagetext' );
154        }
155
156        $permissionErrors = $this->permissionManager->getPermissionErrors(
157            'delete', $this->getUser(), $this->title
158        );
159        if ( count( $permissionErrors ) ) {
160            throw new PermissionsError( 'delete', $permissionErrors );
161        }
162
163        # Check for database lock
164        $this->checkReadOnly();
165
166        // Let the caller know it's safe to continue
167        return true;
168    }
169
170    /**
171     * Checks token. Use before real actions happen. Have to use wpEditToken
172     * for compatibility for SpecialMovepage.php.
173     */
174    private function checkToken(): bool {
175        return $this->getContext()->getCsrfTokenSet()->matchTokenField( 'wpEditToken' );
176    }
177
178    /** The query form. */
179    private function showForm(): void {
180        $out = $this->getOutput();
181        $out->addBacklinkSubtitle( $this->title );
182        $out->addWikiMsg( 'pt-deletepage-intro', self::LOG_PAGE[ $this->entityType ] );
183
184        $formDescriptor = $this->getCommonFormFields();
185
186        HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
187            ->setAction( $this->getPageTitle( $this->text )->getLocalURL() )
188            ->setSubmitName( 'subaction' )
189            ->setSubmitTextMsg( 'pt-deletepage-action-check' )
190            ->setWrapperLegendMsg( 'pt-deletepage-any-legend' )
191            ->prepareForm()
192            ->displayForm( false );
193    }
194
195    /**
196     * The second form, which still allows changing some things.
197     * Lists all the action which would take place.
198     */
199    private function showConfirmation(): void {
200        $out = $this->getOutput();
201        $count = 0;
202        $subpageCount = 0;
203
204        $out->addBacklinkSubtitle( $this->title );
205        $out->addWikiMsg( 'pt-deletepage-intro', self::LOG_PAGE[ $this->entityType ] );
206
207        $subpages = $this->bundleDeleter->getPagesForDeletion( $this->title, $this->code, $this->isTranslation() );
208
209        $out->wrapWikiMsg( '== $1 ==', 'pt-deletepage-list-pages' );
210
211        if ( !$this->isTranslation() ) {
212            $count++;
213            $out->addWikiTextAsInterface(
214                $this->getChangeLine( $this->title )
215            );
216        }
217
218        $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-translation' );
219        $lines = [];
220        foreach ( $subpages[ 'translationPages' ] as $old ) {
221            $count++;
222            $lines[] = $this->getChangeLine( $old );
223        }
224        $this->listPages( $out, $lines );
225
226        $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-section' );
227
228        $lines = [];
229        foreach ( $subpages[ 'translationUnitPages' ] as $old ) {
230            $count++;
231            $lines[] = $this->getChangeLine( $old );
232        }
233        $this->listPages( $out, $lines );
234
235        if ( Utilities::allowsSubpages( $this->title ) ) {
236            $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-other' );
237            $subpages = $subpages[ 'normalSubpages' ];
238            $lines = [];
239            foreach ( $subpages as $old ) {
240                $subpageCount++;
241                $lines[] = $this->getChangeLine( $old );
242            }
243            $this->listPages( $out, $lines );
244        }
245
246        $totalPageCount = $count + $subpageCount;
247
248        $out->addWikiTextAsInterface( "----\n" );
249        $out->addWikiMsg(
250            'pt-deletepage-list-count',
251            $this->getLanguage()->formatNum( $totalPageCount ),
252            $this->getLanguage()->formatNum( $subpageCount )
253        );
254
255        $formDescriptor = $this->getCommonFormFields();
256        $formDescriptor['subpages'] = [
257            'type' => 'check',
258            'name' => 'subpages',
259            'id' => 'mw-subpages',
260            'label' => $this->msg( 'pt-deletepage-subpages' )->text(),
261            'default' => $this->doSubpages,
262        ];
263
264        $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
265        $htmlForm
266            ->setWrapperLegendMsg(
267                $this->msg( self::WRAPPER_LEGEND_MSG[ $this->entityType ], $this->title->getPrefixedText() )
268            )
269            ->setAction( $this->getPageTitle( $this->text )->getLocalURL() )
270            ->setSubmitTextMsg( 'pt-deletepage-action-perform' )
271            ->setSubmitName( 'subaction' )
272            ->setSubmitDestructive()
273            ->addButton( [
274                'name' => 'subaction',
275                'value' => $this->msg( 'pt-deletepage-action-other' )->text(),
276            ] )
277            ->prepareForm()
278            ->displayForm( false );
279    }
280
281    /** @return string One line of wikitext, without trailing newline. */
282    private function getChangeLine( Title $title ): string {
283        return '* ' . $title->getPrefixedText();
284    }
285
286    private function performAction(): void {
287        $this->bundleDeleter->deleteAsynchronously(
288            $this->title,
289            $this->isTranslation(),
290            $this->getUser(),
291            $this->bundleDeleter->getPagesForDeletion( $this->title, $this->code, $this->isTranslation() ),
292            $this->doSubpages,
293            $this->reason
294        );
295
296        $this->getOutput()->addWikiMsg( 'pt-deletepage-started', self::LOG_PAGE[ $this->entityType ] );
297    }
298
299    private function getCommonFormFields(): array {
300        $dropdownOptions = $this->msg( 'deletereason-dropdown' )->inContentLanguage()->text();
301
302        $options = Xml::listDropdownOptions(
303            $dropdownOptions,
304            [
305                'other' => $this->msg( 'pt-deletepage-reason-other' )->inContentLanguage()->text()
306            ]
307        );
308
309        return [
310            'wpTitle' => [
311                'type' => 'text',
312                'name' => 'wpTitle',
313                'label-message' => 'pt-deletepage-current',
314                'size' => 30,
315                'default' => $this->title->getPrefixedText(),
316                'readonly' => true,
317            ],
318            'wpDeleteReasonList' => [
319                'type' => 'select',
320                'name' => 'wpDeleteReasonList',
321                'label-message' => 'pt-deletepage-reason',
322                'options' => $options,
323            ],
324            'wpReason' => [
325                'type' => 'text',
326                'name' => 'wpReason',
327                'label-message' => 'pt-deletepage-reason-details',
328                'default' => $this->reason,
329            ]
330        ];
331    }
332
333    private function listPages( OutputPage $out, array $lines ): void {
334        if ( $lines ) {
335            $out->addWikiTextAsInterface( implode( "\n", $lines ) );
336        } else {
337            $out->addWikiMsg( 'pt-deletepage-list-no-pages' );
338        }
339    }
340
341    private function getDeleteReason( WebRequest $request ): string {
342        $dropdownSelection = $request->getText( 'wpDeleteReasonList', 'other' );
343        $reasonInput = $request->getText( 'wpReason' );
344
345        if ( $dropdownSelection === 'other' ) {
346            return $reasonInput;
347        } elseif ( $reasonInput !== '' ) {
348            // Entry from drop down menu + additional comment
349            $separator = $this->msg( 'colon-separator' )->inContentLanguage()->text();
350            return "$dropdownSelection$separator$reasonInput";
351        } else {
352            return $dropdownSelection;
353        }
354    }
355
356    /** Indentify type of entity being deleted: messagebundle, translatablepage, or translations */
357    private function identifyEntityType(): ?string {
358        $bundle = $this->bundleFactory->getBundle( $this->title );
359        if ( $bundle ) {
360            if ( $bundle instanceof MessageBundle ) {
361                return 'messagebundle';
362            } else {
363                return 'translatablepage';
364            }
365        } elseif ( TranslatablePage::isTranslationPage( $this->title ) ) {
366            return 'translationpage';
367        }
368
369        return null;
370    }
371
372    private function isTranslation(): bool {
373        return $this->entityType === 'translationpage';
374    }
375}