Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 189 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
DeleteTranslatableBundleSpecialPage | |
0.00% |
0 / 189 |
|
0.00% |
0 / 14 |
1892 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
240 | |||
doBasicChecks | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
checkToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
showForm | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
showConfirmation | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
42 | |||
getChangeLine | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
performAction | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getCommonFormFields | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
2 | |||
listPages | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getDeleteReason | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
identifyEntityType | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
isTranslation | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\PageTranslation; |
5 | |
6 | use ErrorPageError; |
7 | use HTMLForm; |
8 | use MediaWiki\Extension\Translate\MessageBundleTranslation\MessageBundle; |
9 | use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundleFactory; |
10 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
11 | use MediaWiki\Permissions\PermissionManager; |
12 | use MediaWiki\Title\Title; |
13 | use OutputPage; |
14 | use PermissionsError; |
15 | use ReadOnlyError; |
16 | use UnlistedSpecialPage; |
17 | use WebRequest; |
18 | use 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 | */ |
26 | class 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 | } |