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->reason,
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()->addBacklinkSubtitle( $this->oldTitle );
218 $this->getOutput()->addWikiMsg(
219 'pt-movepage-intro',
220 $this->getLogPageWikiLink(
221 $this->bundleFactory->getBundle( Title::newFromText( $this->oldText ) )
222 )
223 );
224
225 HTMLForm::factory( 'ooui', $this->getCommonFormFields(), $this->getContext() )
226 ->setMethod( 'post' )
227 ->setAction( $this->getPageTitle( $this->oldText )->getLocalURL() )
228 ->setSubmitName( 'subaction' )
229 ->setSubmitTextMsg( 'pt-movepage-action-check' )
230 ->setWrapperLegendMsg(
231 $bundle instanceof MessageBundle ? 'pt-movepage-messagebundle-legend' : 'pt-movepage-legend'
232 )
233 ->prepareForm()
234 ->displayForm( false );
235 }
236
241 protected function showConfirmation( PageMoveCollection $pageCollection, TranslatableBundle $bundle ): void {
242 $out = $this->getOutput();
243 $out->addBacklinkSubtitle( $this->oldTitle );
244 $out->addWikiMsg(
245 'pt-movepage-intro',
246 $this->getLogPageWikiLink(
247 $this->bundleFactory->getBundle( Title::newFromText( $this->oldText ) )
248 )
249 );
250
251 $count = 0;
252 $subpagesCount = 0;
253 $talkpagesCount = 0;
254
256 $pagesToMove = [
257 'pt-movepage-list-pages' => [ $pageCollection->getTranslatablePage() ],
258 'pt-movepage-list-translation' => $pageCollection->getTranslationPagesPair(),
259 'pt-movepage-list-section' => $pageCollection->getUnitPagesPair()
260 ];
261
262 $subpages = $pageCollection->getSubpagesPair();
263 if ( $subpages ) {
264 $pagesToMove[ 'pt-movepage-list-other'] = $subpages;
265 }
266
267 foreach ( $pagesToMove as $type => $pages ) {
268 $this->addSectionHeaderAndMessage( $out, $type, $pages );
269
270 if ( !$pages ) {
271 continue;
272 }
273
274 $lines = [];
275
276 foreach ( $pages as $pagePairs ) {
277 $count++;
278
279 if ( $type === 'pt-movepage-list-other' ) {
280 $subpagesCount++;
281 }
282
283 $old = $pagePairs->getOldTitle();
284 $new = $pagePairs->getNewTitle();
285 $line = '* ' . $old->getPrefixedText() . ' → ' . $new->getPrefixedText();
286 if ( $pagePairs->hasTalkpage() ) {
287 $count++;
288 $talkpagesCount++;
289 $line .= ' ' . $this->msg( 'pt-movepage-talkpage-exists' )->text();
290 }
291
292 $lines[] = $line;
293 }
294
295 $out->addWikiTextAsInterface( implode( "\n", $lines ) );
296 }
297
298 $translatableSubpages = $pageCollection->getTranslatableSubpages();
299 $sectionType = 'pt-movepage-list-translatable';
300 $this->addSectionHeaderAndMessage( $out, $sectionType, $translatableSubpages );
301 if ( $translatableSubpages ) {
302 $lines = [];
303 $out->wrapWikiMsg( "'''$1'''", $this->msg( 'pt-movepage-list-translatable-note' ) );
304 foreach ( $translatableSubpages as $page ) {
305 $lines[] = '* ' . $page->getPrefixedText();
306 }
307 $out->addWikiTextAsInterface( implode( "\n", $lines ) );
308 }
309
310 $out->addWikiTextAsInterface( "----\n" );
311 $out->addWikiMsg(
312 'pt-movepage-list-count',
313 $this->getLanguage()->formatNum( $count ),
314 $this->getLanguage()->formatNum( $subpagesCount ),
315 $this->getLanguage()->formatNum( $talkpagesCount )
316 );
317
318 $formDescriptor = array_merge(
319 $this->getCommonFormFields(),
320 [
321 'subpages' => [
322 'type' => 'check',
323 'name' => 'subpages',
324 'id' => 'mw-subpages',
325 'label-message' => 'pt-movepage-subpages',
326 'default' => $this->moveSubpages,
327 ],
328 'talkpages' => [
329 'type' => 'check',
330 'name' => 'talkpages',
331 'id' => 'mw-talkpages',
332 'label-message' => 'pt-movepage-talkpages',
333 'default' => $this->moveTalkpages
334 ]
335 ]
336 );
337
338 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
339 $htmlForm
340 ->addButton( [
341 'name' => 'subaction',
342 'value' => $this->msg( 'pt-movepage-action-other' )->text(),
343 ] )
344 ->setMethod( 'post' )
345 ->setAction( $this->getPageTitle( $this->oldText )->getLocalURL() )
346 ->setSubmitName( 'subaction' )
347 ->setSubmitTextMsg( 'pt-movepage-action-perform' )
348 ->setWrapperLegendMsg(
349 $bundle instanceof MessageBundle ? 'pt-movepage-messagebundle-legend' : 'pt-movepage-legend'
350 )
351 ->prepareForm()
352 ->displayForm( false );
353 }
354
356 private function addSectionHeaderAndMessage( OutputPage $out, string $type, array $pages ): void {
357 $pageCount = count( $pages );
358 $out->wrapWikiMsg( '=== $1 ===', [ $type, $pageCount ] );
359
360 if ( !$pageCount ) {
361 $out->addWikiMsg( 'pt-movepage-list-no-pages' );
362 }
363 }
364
365 private function getSubactionFromRequest( string $subactionText ): string {
366 switch ( $subactionText ) {
367 case $this->msg( 'pt-movepage-action-check' )->text():
368 return 'check';
369 case $this->msg( 'pt-movepage-action-perform' )->text():
370 return 'perform';
371 case $this->msg( 'pt-movepage-action-other' )->text():
372 return 'show-form';
373 default:
374 return 'show-form';
375 }
376 }
377
378 private function getCommonFormFields(): array {
379 return [
380 'wpOldTitle' => [
381 'type' => 'text',
382 'name' => 'wpOldTitle',
383 'label-message' => 'pt-movepage-current',
384 'default' => $this->oldText,
385 'readonly' => true,
386 ],
387 'wpNewTitle' => [
388 'type' => 'text',
389 'name' => 'wpNewTitle',
390 'label-message' => 'pt-movepage-new',
391 ],
392 'reason' => [
393 'type' => 'text',
394 'name' => 'reason',
395 'label-message' => 'pt-movepage-reason',
396 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
397 'default' => $this->reason,
398 ],
399 'subpages' => [
400 'type' => 'hidden',
401 'name' => 'subpages',
402 'default' => $this->moveSubpages,
403 ],
404 'talkpages' => [
405 'type' => 'hidden',
406 'name' => 'talkpages',
407 'default' => $this->moveTalkpages
408 ]
409 ];
410 }
411
412 private function getSpecialPageTitle( TranslatableBundle $bundle ): Message {
413 if ( $bundle instanceof TranslatablePage ) {
414 return $this->msg( 'pt-movepage-title', $this->oldText );
415 } elseif ( $bundle instanceof MessageBundle ) {
416 return $this->msg( 'pt-movepage-messagebundle-title', $this->oldText );
417 }
418
419 throw new InvalidArgumentException( 'TranslatableBundle is neither a TranslatablePage or MessageBundle' );
420 }
421
422 private function getLogPageWikiLink( ?TranslatableBundle $bundle ): string {
423 if ( $bundle instanceof MessageBundle ) {
424 return 'Special:Log/messagebundle';
425 }
426
427 // Default to page translation log in case of errors
428 return 'Special:Log/pagetranslation';
429 }
430}
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'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, '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:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, '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:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, '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:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, '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(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(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(), $services->getDBLoadBalancer());}, '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.