Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
DeleteTranslatableBundleSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\PageTranslation;
5
6use BagOStuff;
7use ErrorPageError;
8use HTMLForm;
9use JobQueueGroup;
16use MediaWiki\Permissions\PermissionManager;
17use OutputPage;
18use PermissionsError;
19use ReadOnlyError;
20use SpecialPage;
21use Title;
22use WebRequest;
23use Xml;
24
31class DeleteTranslatableBundleSpecialPage extends SpecialPage {
32 // Basic form parameters both as text and as titles
33 private string $text;
34 private ?Title $title;
35 // Other form parameters
37 private string $reason;
39 private bool $doSubpages = false;
41 private ?string $code;
42 private BagOStuff $mainCache;
43 private PermissionManager $permissionManager;
44 private TranslatableBundleFactory $bundleFactory;
45 private SubpageListBuilder $subpageBuilder;
46 private JobQueueGroup $jobQueueGroup;
47 private string $entityType;
48 private const PAGE_TITLE_MSG = [
49 'messagebundle' => 'pt-deletepage-mb-title',
50 'translatablepage' => 'pt-deletepage-tp-title',
51 'translationpage' => 'pt-deletepage-lang-title'
52 ];
53 private const WRAPPER_LEGEND_MSG = [
54 'messagebundle' => 'pt-deletepage-mb-legend',
55 'translatablepage' => 'pt-deletepage-tp-title',
56 'translationpage' => 'pt-deletepage-tp-legend'
57 ];
58 private const LOG_PAGE = [
59 'messagebundle' => 'Special:Log/messagebundle',
60 'translatablepage' => 'Special:Log/pagetranslation',
61 'translationpage' => 'Special:Log/pagetranslation'
62 ];
63
64 public function __construct(
65 BagOStuff $mainCache,
66 PermissionManager $permissionManager,
67 TranslatableBundleFactory $bundleFactory,
68 SubpageListBuilder $subpageBuilder,
69 JobQueueGroup $jobQueueGroup
70 ) {
71 parent::__construct( 'PageTranslationDeletePage', 'pagetranslation' );
72 $this->mainCache = $mainCache;
73 $this->permissionManager = $permissionManager;
74 $this->bundleFactory = $bundleFactory;
75 $this->subpageBuilder = $subpageBuilder;
76 $this->jobQueueGroup = $jobQueueGroup;
77 }
78
79 public function doesWrites() {
80 return true;
81 }
82
83 public function isListed() {
84 return false;
85 }
86
87 public function execute( $par ) {
88 $this->addHelpLink( 'Help:Deletion_and_undeletion' );
89
90 $request = $this->getRequest();
91
92 $par = (string)$par;
93
94 // Yes, the use of getVal() and getText() is wanted, see bug T22365
95 $this->text = $request->getVal( 'wpTitle', $par );
96 $this->title = Title::newFromText( $this->text );
97 $this->reason = $this->getDeleteReason( $request );
98 $this->doSubpages = $request->getBool( 'subpages' );
99
100 if ( !$this->doBasicChecks() ) {
101 return;
102 }
103
104 $out = $this->getOutput();
105
106 // Real stuff starts here
107 $entityType = $this->identifyEntityType();
108 if ( !$entityType ) {
109 throw new ErrorPageError( 'pt-deletepage-invalid-title', 'pt-deletepage-invalid-text' );
110 }
111 $this->entityType = $entityType;
112
113 if ( $this->isTranslation() ) {
114 [ , $this->code ] = Utilities::figureMessage( $this->title->getText() );
115 } else {
116 $this->code = null;
117 }
118
119 $out->setPageTitle(
120 $this->msg( self::PAGE_TITLE_MSG[ $this->entityType ], $this->title->getPrefixedText() )
121 );
122
123 if ( !$this->getUser()->isAllowed( 'pagetranslation' ) ) {
124 throw new PermissionsError( 'pagetranslation' );
125 }
126
127 // Is there really no better way to do this?
128 $subactionText = $request->getText( 'subaction' );
129 switch ( $subactionText ) {
130 case $this->msg( 'pt-deletepage-action-check' )->text():
131 $subaction = 'check';
132 break;
133 case $this->msg( 'pt-deletepage-action-perform' )->text():
134 $subaction = 'perform';
135 break;
136 case $this->msg( 'pt-deletepage-action-other' )->text():
137 $subaction = '';
138 break;
139 default:
140 $subaction = '';
141 }
142
143 if ( $subaction === 'check' && $this->checkToken() && $request->wasPosted() ) {
144 $this->showConfirmation();
145 } elseif ( $subaction === 'perform' && $this->checkToken() && $request->wasPosted() ) {
146 $this->performAction();
147 } else {
148 $this->showForm();
149 }
150 }
151
157 private function doBasicChecks(): bool {
158 // Check rights
159 if ( !$this->userCanExecute( $this->getUser() ) ) {
160 $this->displayRestrictionError();
161 }
162
163 if ( $this->title === null ) {
164 throw new ErrorPageError( 'notargettitle', 'notargettext' );
165 }
166
167 if ( !$this->title->exists() ) {
168 throw new ErrorPageError( 'nopagetitle', 'nopagetext' );
169 }
170
171 $permissionErrors = $this->permissionManager->getPermissionErrors(
172 'delete', $this->getUser(), $this->title
173 );
174 if ( count( $permissionErrors ) ) {
175 throw new PermissionsError( 'delete', $permissionErrors );
176 }
177
178 # Check for database lock
179 $this->checkReadOnly();
180
181 // Let the caller know it's safe to continue
182 return true;
183 }
184
189 private function checkToken(): bool {
190 return $this->getContext()->getCsrfTokenSet()->matchTokenField( 'wpEditToken' );
191 }
192
194 private function showForm(): void {
195 $out = $this->getOutput();
196 $out->addBacklinkSubtitle( $this->title );
197 $out->addWikiMsg( 'pt-deletepage-intro', self::LOG_PAGE[ $this->entityType ] );
198
199 $formDescriptor = $this->getCommonFormFields();
200
201 HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
202 ->setAction( $this->getPageTitle( $this->text )->getLocalURL() )
203 ->setSubmitName( 'subaction' )
204 ->setSubmitTextMsg( 'pt-deletepage-action-check' )
205 ->setWrapperLegendMsg( 'pt-deletepage-any-legend' )
206 ->prepareForm()
207 ->displayForm( false );
208 }
209
214 private function showConfirmation(): void {
215 $out = $this->getOutput();
216 $count = 0;
217 $subpageCount = 0;
218
219 $out->addBacklinkSubtitle( $this->title );
220 $out->addWikiMsg( 'pt-deletepage-intro', self::LOG_PAGE[ $this->entityType ] );
221
222 $subpages = $this->getPagesForDeletion();
223
224 $out->wrapWikiMsg( '== $1 ==', 'pt-deletepage-list-pages' );
225
226 if ( !$this->isTranslation() ) {
227 $count++;
228 $out->addWikiTextAsInterface(
229 $this->getChangeLine( $this->title )
230 );
231 }
232
233 $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-translation' );
234 $lines = [];
235 foreach ( $subpages[ 'translationPages' ] as $old ) {
236 $count++;
237 $lines[] = $this->getChangeLine( $old );
238 }
239 $this->listPages( $out, $lines );
240
241 $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-section' );
242
243 $lines = [];
244 foreach ( $subpages[ 'translationUnitPages' ] as $old ) {
245 $count++;
246 $lines[] = $this->getChangeLine( $old );
247 }
248 $this->listPages( $out, $lines );
249
250 if ( Utilities::allowsSubpages( $this->title ) ) {
251 $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-other' );
252 $subpages = $subpages[ 'normalSubpages' ];
253 $lines = [];
254 foreach ( $subpages as $old ) {
255 $subpageCount++;
256 $lines[] = $this->getChangeLine( $old );
257 }
258 $this->listPages( $out, $lines );
259 }
260
261 $totalPageCount = $count + $subpageCount;
262
263 $out->addWikiTextAsInterface( "----\n" );
264 $out->addWikiMsg(
265 'pt-deletepage-list-count',
266 $this->getLanguage()->formatNum( $totalPageCount ),
267 $this->getLanguage()->formatNum( $subpageCount )
268 );
269
270 $formDescriptor = $this->getCommonFormFields();
271 $formDescriptor['subpages'] = [
272 'type' => 'check',
273 'name' => 'subpages',
274 'id' => 'mw-subpages',
275 'label' => $this->msg( 'pt-deletepage-subpages' )->text(),
276 'default' => $this->doSubpages,
277 ];
278
279 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
280 $htmlForm
281 ->setWrapperLegendMsg(
282 $this->msg( self::WRAPPER_LEGEND_MSG[ $this->entityType ], $this->title->getPrefixedText() )
283 )
284 ->setAction( $this->getPageTitle( $this->text )->getLocalURL() )
285 ->setSubmitTextMsg( 'pt-deletepage-action-perform' )
286 ->setSubmitName( 'subaction' )
287 ->setSubmitDestructive()
288 ->addButton( [
289 'name' => 'subaction',
290 'value' => $this->msg( 'pt-deletepage-action-other' )->text(),
291 ] )
292 ->prepareForm()
293 ->displayForm( false );
294 }
295
297 private function getChangeLine( Title $title ): string {
298 return '* ' . $title->getPrefixedText();
299 }
300
301 private function performAction(): void {
302 $jobs = [];
303 $target = $this->title;
304 $base = $this->title->getPrefixedText();
305 $isTranslation = $this->isTranslation();
306 $subpageList = $this->getPagesForDeletion();
307 $bundle = $this->getValidBundleFromTitle();
308 $bundleType = get_class( $bundle );
309
310 $user = $this->getUser();
311 foreach ( $subpageList[ 'translationPages' ] as $old ) {
312 $jobs[$old->getPrefixedText()] = DeleteTranslatableBundleJob::newJob(
313 $old, $base, $bundleType, $isTranslation, $user, $this->reason
314 );
315 }
316
317 foreach ( $subpageList[ 'translationUnitPages' ] as $old ) {
318 $jobs[$old->getPrefixedText()] = DeleteTranslatableBundleJob::newJob(
319 $old, $base, $bundleType, $isTranslation, $user, $this->reason
320 );
321 }
322
323 if ( $this->doSubpages ) {
324 foreach ( $subpageList[ 'normalSubpages' ] as $old ) {
325 $jobs[$old->getPrefixedText()] = DeleteTranslatableBundleJob::newJob(
326 $old, $base, $bundleType, $isTranslation, $user, $this->reason
327 );
328 }
329 }
330
331 if ( !$isTranslation ) {
332 $jobs[$this->title->getPrefixedText()] = DeleteTranslatableBundleJob::newJob(
333 $this->title, $base, $bundleType, $isTranslation, $user, $this->reason
334 );
335 }
336
337 $this->jobQueueGroup->push( $jobs );
338
339 $this->mainCache->set(
340 $this->mainCache->makeKey( 'pt-base', $target->getPrefixedText() ),
341 array_keys( $jobs ),
342 6 * $this->mainCache::TTL_HOUR
343 );
344
345 if ( !$isTranslation ) {
346 $this->bundleFactory->getStore( $bundle )->delete( $this->title );
347 }
348
349 $this->getOutput()->addWikiMsg( 'pt-deletepage-started', self::LOG_PAGE[ $this->entityType ] );
350 }
351
352 private function getCommonFormFields(): array {
353 $dropdownOptions = $this->msg( 'deletereason-dropdown' )->inContentLanguage()->text();
354
355 $options = Xml::listDropDownOptions(
356 $dropdownOptions,
357 [
358 'other' => $this->msg( 'pt-deletepage-reason-other' )->inContentLanguage()->text()
359 ]
360 );
361
362 return [
363 'wpTitle' => [
364 'type' => 'text',
365 'name' => 'wpTitle',
366 'label-message' => 'pt-deletepage-current',
367 'size' => 30,
368 'default' => $this->title->getPrefixedText(),
369 'readonly' => true,
370 ],
371 'wpDeleteReasonList' => [
372 'type' => 'select',
373 'name' => 'wpDeleteReasonList',
374 'label-message' => 'pt-deletepage-reason',
375 'options' => $options,
376 ],
377 'wpReason' => [
378 'type' => 'text',
379 'name' => 'wpReason',
380 'label-message' => 'pt-deletepage-reason-details',
381 'default' => $this->reason,
382 ]
383 ];
384 }
385
386 private function listPages( OutputPage $out, array $lines ): void {
387 if ( $lines ) {
388 $out->addWikiTextAsInterface( implode( "\n", $lines ) );
389 } else {
390 $out->addWikiMsg( 'pt-deletepage-list-no-pages' );
391 }
392 }
393
394 private function getDeleteReason( WebRequest $request ): string {
395 $dropdownSelection = $request->getText( 'wpDeleteReasonList', 'other' );
396 $reasonInput = $request->getText( 'wpReason' );
397
398 if ( $dropdownSelection === 'other' ) {
399 return $reasonInput;
400 } elseif ( $reasonInput !== '' ) {
401 // Entry from drop down menu + additional comment
402 $separator = $this->msg( 'colon-separator' )->inContentLanguage()->text();
403 return "$dropdownSelection$separator$reasonInput";
404 } else {
405 return $dropdownSelection;
406 }
407 }
408
409 private function getPagesForDeletion(): array {
410 if ( $this->isTranslation() ) {
411 $resultSet = $this->subpageBuilder->getEmptyResultSet();
412
413 [ $titleKey, ] = Utilities::figureMessage( $this->title->getPrefixedDBkey() );
414 $translatablePage = TranslatablePage::newFromTitle( Title::newFromText( $titleKey ) );
415
416 $resultSet['translationPages'] = [ $this->title ];
417 $resultSet['translationUnitPages'] = $translatablePage->getTranslationUnitPages( $this->code );
418 return $resultSet;
419 } else {
420 $bundle = $this->bundleFactory->getValidBundle( $this->title );
421 return $this->subpageBuilder->getSubpagesPerType( $bundle, false );
422 }
423 }
424
425 private function getValidBundleFromTitle(): TranslatableBundle {
426 $bundleTitle = $this->title;
427 if ( $this->isTranslation() ) {
428 [ $key, ] = Utilities::figureMessage( $this->title->getPrefixedDBkey() );
429 $bundleTitle = Title::newFromText( $key );
430 }
431
432 return $this->bundleFactory->getValidBundle( $bundleTitle );
433 }
434
436 private function identifyEntityType(): ?string {
437 $bundle = $this->bundleFactory->getBundle( $this->title );
438 if ( $bundle ) {
439 if ( $bundle instanceof MessageBundle ) {
440 return 'messagebundle';
441 } else {
442 return 'translatablepage';
443 }
444 } elseif ( TranslatablePage::isTranslationPage( $this->title ) ) {
445 return 'translationpage';
446 }
447
448 return null;
449 }
450
451 private function isTranslation(): bool {
452 return $this->entityType === 'translationpage';
453 }
454}
Generates list of subpages for the translatable bundle that can be moved or deleted.
Create instances of various classes based on the type of TranslatableBundle.
Translatable bundle represents a message group where its translatable content is defined on a wiki pa...
Special page which enables deleting translations of translatable bundles and translation pages.
static newFromTitle(PageIdentity $title)
Constructs a translatable page from title.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31