Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
MoveTranslatableBundleSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\PageTranslation;
5
6use CommentStore;
7use ErrorPageError;
8use Html;
9use HTMLForm;
10use InvalidArgumentException;
14use MediaWiki\Permissions\PermissionManager;
15use Message;
16use OutputPage;
17use PermissionsError;
18use ReadOnlyError;
19use SplObjectStorage;
20use ThrottledError;
21use Title;
22use UnlistedSpecialPage;
23use Wikimedia\ObjectFactory\ObjectFactory;
24
33class MoveTranslatableBundleSpecialPage extends UnlistedSpecialPage {
34 // Form parameters both as text and as titles
36 private $oldText;
38 private $reason;
40 private $moveTalkpages = true;
42 private $moveSubpages = true;
43 // Dependencies
45 private $objectFactory;
47 private $bundleMover;
49 private $permissionManager;
51 private $bundleFactory;
52 private $movePageSpec;
53 // Other
55 private $oldTitle;
56
57 public function __construct(
58 ObjectFactory $objectFactory,
59 PermissionManager $permissionManager,
60 TranslatableBundleMover $bundleMover,
61 TranslatableBundleFactory $bundleFactory,
62 $movePageSpec
63 ) {
64 parent::__construct( 'Movepage' );
65 $this->objectFactory = $objectFactory;
66 $this->permissionManager = $permissionManager;
67 $this->bundleMover = $bundleMover;
68 $this->bundleFactory = $bundleFactory;
69 $this->movePageSpec = $movePageSpec;
70 }
71
72 public function doesWrites(): bool {
73 return true;
74 }
75
76 protected function getGroupName(): string {
77 return 'pagetools';
78 }
79
81 public function execute( $par ) {
82 $request = $this->getRequest();
83 $user = $this->getUser();
84 $this->addHelpLink( 'Help:Extension:Translate/Move_translatable_page' );
85
86 $this->oldText = $request->getText(
87 'wpOldTitle',
88 $request->getText( 'target', $par ?? '' )
89 );
90 $newText = $request->getText( 'wpNewTitle' );
91
92 $this->oldTitle = Title::newFromText( $this->oldText );
93 $newTitle = Title::newFromText( $newText );
94 // Normalize input
95 if ( $this->oldTitle ) {
96 $this->oldText = $this->oldTitle->getPrefixedText();
97 }
98
99 $this->reason = $request->getText( 'reason' );
100
101 // This will throw exceptions if there is an error.
102 $this->doBasicChecks();
103
104 // Real stuff starts here
105 $bundle = $this->bundleFactory->getBundle( $this->oldTitle );
106 if ( $bundle && $bundle->isMoveable() ) {
107 $this->getOutput()->setPageTitle( $this->getSpecialPageTitle( $bundle ) );
108
109 if ( !$user->isAllowed( 'pagetranslation' ) ) {
110 throw new PermissionsError( 'pagetranslation' );
111 }
112
113 $subaction = $this->getSubactionFromRequest( $request->getText( 'subaction' ) );
114
115 $isValidPostRequest = $this->checkToken() && $request->wasPosted();
116 if ( $isValidPostRequest && $subaction === 'check' ) {
117 try {
118 $pageCollection = $this->bundleMover->getPageMoveCollection(
119 $this->oldTitle,
120 $newTitle,
121 $user,
122 $this->reason,
123 $this->moveSubpages,
124 $this->moveTalkpages
125 );
126 } catch ( ImpossiblePageMove $e ) {
127 $this->showErrors( $e->getBlockers() );
128 $this->showForm( $bundle );
129 return;
130 }
131
132 $this->showConfirmation( $pageCollection, $bundle );
133 } elseif ( $isValidPostRequest && $subaction === 'perform' ) {
134 $this->moveSubpages = $request->getBool( 'subpages' );
135 $this->moveTalkpages = $request->getBool( 'talkpages' );
136
137 $this->bundleMover->moveAsynchronously(
138 $this->oldTitle,
139 $newTitle,
140 $this->moveSubpages,
141 $this->getUser(),
142 $this->msg( 'pt-movepage-logreason', $this->oldTitle )->inContentLanguage()->text(),
143 $this->moveTalkpages
144 );
145 $this->getOutput()->addWikiMsg(
146 'pt-movepage-started',
147 $this->getLogPageWikiLink( $this->bundleFactory->getValidBundle( $this->oldTitle ) )
148 );
149 } else {
150 $this->showForm( $bundle );
151 }
152 } else {
153 // Delegate... don't want to reimplement this
154 $sp = $this->objectFactory->createObject( $this->movePageSpec );
155 $sp->execute( $par );
156 }
157 }
158
164 protected function doBasicChecks(): void {
165 $this->checkReadOnly();
166
167 if ( $this->oldTitle === null ) {
168 throw new ErrorPageError( 'notargettitle', 'notargettext' );
169 }
170
171 if ( !$this->oldTitle->exists() ) {
172 throw new ErrorPageError( 'nopagetitle', 'nopagetext' );
173 }
174
175 if ( $this->getUser()->pingLimiter( 'move' ) ) {
176 throw new ThrottledError;
177 }
178
179 // Check rights
180 $permErrors = $this->permissionManager
181 ->getPermissionErrors( 'move', $this->getUser(), $this->oldTitle );
182 if ( count( $permErrors ) ) {
183 throw new PermissionsError( 'move', $permErrors );
184 }
185 }
186
188 protected function checkToken(): bool {
189 return $this->getContext()->getCsrfTokenSet()->matchTokenField( 'wpEditToken' );
190 }
191
193 protected function showErrors( SplObjectStorage $errors ): void {
194 // If there are many errors, for performance reasons we must parse them all at once
195 $s = '';
196 $context = 'pt-movepage-error-placeholder';
197 foreach ( $errors as $title ) {
198 $titleText = $title->getPrefixedText();
199 $s .= "'''$titleText'''\n\n";
200 $s .= $errors[ $title ]->getWikiText( false, $context );
201 }
202
203 $out = $this->getOutput();
204 $out->addHTML(
205 Html::errorBox(
206 $out->msg(
207 'pt-movepage-blockers',
208 $this->getLanguage()->formatNum( count( $errors ) )
209 )->parseAsBlock() .
210 $out->parseAsContent( $s )
211 )
212 );
213 }
214
216 public function showForm( TranslatableBundle $bundle ) {
217 $this->getOutput()->addWikiMsg(
218 'pt-movepage-intro',
219 $this->getLogPageWikiLink(
220 $this->bundleFactory->getBundle( Title::newFromText( $this->oldText ) )
221 )
222 );
223
224 HTMLForm::factory( 'ooui', $this->getCommonFormFields(), $this->getContext() )
225 ->setMethod( 'post' )
226 ->setAction( $this->getPageTitle( $this->oldText )->getLocalURL() )
227 ->setSubmitName( 'subaction' )
228 ->setSubmitTextMsg( 'pt-movepage-action-check' )
229 ->setWrapperLegendMsg(
230 $bundle instanceof MessageBundle ? 'pt-movepage-messagebundle-legend' : 'pt-movepage-legend'
231 )
232 ->prepareForm()
233 ->displayForm( false );
234 }
235
240 protected function showConfirmation( PageMoveCollection $pageCollection, TranslatableBundle $bundle ): void {
241 $out = $this->getOutput();
242
243 $out->addWikiMsg(
244 'pt-movepage-intro',
245 $this->getLogPageWikiLink(
246 $this->bundleFactory->getBundle( Title::newFromText( $this->oldText ) )
247 )
248 );
249
250 $count = 0;
251 $subpagesCount = 0;
252 $talkpagesCount = 0;
253
255 $pagesToMove = [
256 'pt-movepage-list-pages' => [ $pageCollection->getTranslatablePage() ],
257 'pt-movepage-list-translation' => $pageCollection->getTranslationPagesPair(),
258 'pt-movepage-list-section' => $pageCollection->getUnitPagesPair()
259 ];
260
261 $subpages = $pageCollection->getSubpagesPair();
262 if ( $subpages ) {
263 $pagesToMove[ 'pt-movepage-list-other'] = $subpages;
264 }
265
266 foreach ( $pagesToMove as $type => $pages ) {
267 $this->addSectionHeaderAndMessage( $out, $type, $pages );
268
269 if ( !$pages ) {
270 continue;
271 }
272
273 $lines = [];
274
275 foreach ( $pages as $pagePairs ) {
276 $count++;
277
278 if ( $type === 'pt-movepage-list-other' ) {
279 $subpagesCount++;
280 }
281
282 $old = $pagePairs->getOldTitle();
283 $new = $pagePairs->getNewTitle();
284 $line = '* ' . $old->getPrefixedText() . ' → ' . $new->getPrefixedText();
285 if ( $pagePairs->hasTalkpage() ) {
286 $count++;
287 $talkpagesCount++;
288 $line .= ' ' . $this->msg( 'pt-movepage-talkpage-exists' )->text();
289 }
290
291 $lines[] = $line;
292 }
293
294 $out->addWikiTextAsInterface( implode( "\n", $lines ) );
295 }
296
297 $translatableSubpages = $pageCollection->getTranslatableSubpages();
298 $sectionType = 'pt-movepage-list-translatable';
299 $this->addSectionHeaderAndMessage( $out, $sectionType, $translatableSubpages );
300 if ( $translatableSubpages ) {
301 $lines = [];
302 $out->wrapWikiMsg( "'''$1'''", $this->msg( 'pt-movepage-list-translatable-note' ) );
303 foreach ( $translatableSubpages as $page ) {
304 $lines[] = '* ' . $page->getPrefixedText();
305 }
306 $out->addWikiTextAsInterface( implode( "\n", $lines ) );
307 }
308
309 $out->addWikiTextAsInterface( "----\n" );
310 $out->addWikiMsg(
311 'pt-movepage-list-count',
312 $this->getLanguage()->formatNum( $count ),
313 $this->getLanguage()->formatNum( $subpagesCount ),
314 $this->getLanguage()->formatNum( $talkpagesCount )
315 );
316
317 $formDescriptor = array_merge(
318 $this->getCommonFormFields(),
319 [
320 'subpages' => [
321 'type' => 'check',
322 'name' => 'subpages',
323 'id' => 'mw-subpages',
324 'label-message' => 'pt-movepage-subpages',
325 'default' => $this->moveSubpages,
326 ],
327 'talkpages' => [
328 'type' => 'check',
329 'name' => 'talkpages',
330 'id' => 'mw-talkpages',
331 'label-message' => 'pt-movepage-talkpages',
332 'default' => $this->moveTalkpages
333 ]
334 ]
335 );
336
337 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
338 $htmlForm
339 ->addButton( [
340 'name' => 'subaction',
341 'value' => $this->msg( 'pt-movepage-action-other' )->text(),
342 ] )
343 ->setMethod( 'post' )
344 ->setAction( $this->getPageTitle( $this->oldText )->getLocalURL() )
345 ->setSubmitName( 'subaction' )
346 ->setSubmitTextMsg( 'pt-movepage-action-perform' )
347 ->setWrapperLegendMsg(
348 $bundle instanceof MessageBundle ? 'pt-movepage-messagebundle-legend' : 'pt-movepage-legend'
349 )
350 ->prepareForm()
351 ->displayForm( false );
352 }
353
355 private function addSectionHeaderAndMessage( OutputPage $out, string $type, array $pages ): void {
356 $pageCount = count( $pages );
357 $out->wrapWikiMsg( '=== $1 ===', [ $type, $pageCount ] );
358
359 if ( !$pageCount ) {
360 $out->addWikiMsg( 'pt-movepage-list-no-pages' );
361 }
362 }
363
364 private function getSubactionFromRequest( string $subactionText ): string {
365 switch ( $subactionText ) {
366 case $this->msg( 'pt-movepage-action-check' )->text():
367 return 'check';
368 case $this->msg( 'pt-movepage-action-perform' )->text():
369 return 'perform';
370 case $this->msg( 'pt-movepage-action-other' )->text():
371 return 'show-form';
372 default:
373 return 'show-form';
374 }
375 }
376
377 private function getCommonFormFields(): array {
378 return [
379 'wpOldTitle' => [
380 'type' => 'text',
381 'name' => 'wpOldTitle',
382 'label-message' => 'pt-movepage-current',
383 'default' => $this->oldText,
384 'readonly' => true,
385 ],
386 'wpNewTitle' => [
387 'type' => 'text',
388 'name' => 'wpNewTitle',
389 'label-message' => 'pt-movepage-new',
390 ],
391 'reason' => [
392 'type' => 'text',
393 'name' => 'reason',
394 'label-message' => 'pt-movepage-reason',
395 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
396 'default' => $this->reason,
397 ],
398 'subpages' => [
399 'type' => 'hidden',
400 'name' => 'subpages',
401 'default' => $this->moveSubpages,
402 ],
403 'talkpages' => [
404 'type' => 'hidden',
405 'name' => 'talkpages',
406 'default' => $this->moveTalkpages
407 ]
408 ];
409 }
410
411 private function getSpecialPageTitle( TranslatableBundle $bundle ): Message {
412 if ( $bundle instanceof TranslatablePage ) {
413 return $this->msg( 'pt-movepage-title', $this->oldText );
414 } elseif ( $bundle instanceof MessageBundle ) {
415 return $this->msg( 'pt-movepage-messagebundle-title', $this->oldText );
416 }
417
418 throw new InvalidArgumentException( 'TranslatableBundle is neither a TranslatablePage or MessageBundle' );
419 }
420
421 private function getLogPageWikiLink( ?TranslatableBundle $bundle ): string {
422 if ( $bundle instanceof MessageBundle ) {
423 return 'Special:Log/messagebundle';
424 }
425
426 // Default to page translation log in case of errors
427 return 'Special:Log/pagetranslation';
428 }
429}
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), MessageIndex::singleton());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), new RevTagStore(), $services->getDBLoadBalancer());}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
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...
Exception thrown when a translatable page move is not possible.
Replacement for Special:Movepage to allow renaming a translatable bundle and all pages associated wit...
showConfirmation(PageMoveCollection $pageCollection, TranslatableBundle $bundle)
The second form, which still allows changing some things.
doBasicChecks()
Do the basic checks whether moving is possible and whether the input looks anywhere near sane.
Collection of pages potentially affected by a page move operation.
Contains the core logic to validate and move translatable bundles.