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