Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
15.45% |
119 / 770 |
|
10.26% |
4 / 39 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
15.45% |
119 / 770 |
|
10.26% |
4 / 39 |
24860.89 | |
0.00% |
0 / 1 |
renderTagPage | |
59.57% |
28 / 47 |
|
0.00% |
0 / 1 |
12.23 | |||
preprocessTagPage | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
3.00 | |||
onParserOutputPostCacheTransform | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
2.50 | |||
fetchTranslatableTemplateAndTitle | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
132 | |||
onPageContentLanguage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
onTitleGetEditNotices | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
onBeforePageDisplay | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
56 | |||
onVisualEditorBeforeEditor | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onSectionSave | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
updateTranslationPage | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
onGetMagicVariableIDs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onParserGetVariableValueSwitch | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
languages | |
0.00% |
0 / 99 |
|
0.00% |
0 / 1 |
90 | |||
tpProgressIcon | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getTranslatablePageStatus | |
83.33% |
15 / 18 |
|
0.00% |
0 / 1 |
7.23 | |||
addLanguageLinks | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
90 | |||
formatLanguageLink | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
tpSyntaxCheckForEditContent | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
tpSyntaxError | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
5.51 | |||
tpSyntaxCheck | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
2.01 | |||
addTranstagAfterSave | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
3.01 | |||
updateTranstagOnNullRevisions | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
onGetUserPermissionsErrorsExpensive | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
210 | |||
checkTranslatablePageSlow | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
getTranslationRestrictions | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
preventDirectEditing | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
72 | |||
disableDelete | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
translatablePageHeader | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
sourcePageHeader | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
72 | |||
getTranslateLink | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
translationPageHeader | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
42 | |||
replaceMovePage | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
2.00 | |||
lockedPagesCheck | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
replaceSubtitle | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
132 | |||
translateTab | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
56 | |||
onMovePageTranslationUnits | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
90 | |||
onDeleteTranslationUnit | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
56 | |||
onReplaceTextFilterPageTitlesForEdit | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
onReplaceTextFilterPageTitlesForRename | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Translate\PageTranslation; |
4 | |
5 | use Article; |
6 | use Content; |
7 | use DeferredUpdates; |
8 | use Exception; |
9 | use IContextSource; |
10 | use IDBAccessObject; |
11 | use Language; |
12 | use LanguageCode; |
13 | use ManualLogEntry; |
14 | use MediaWiki\CommentStore\CommentStoreComment; |
15 | use MediaWiki\Extension\Translate\MessageBundleTranslation\MessageBundleMessageGroup; |
16 | use MediaWiki\Extension\Translate\MessageLoading\MessageHandle; |
17 | use MediaWiki\Extension\Translate\Services; |
18 | use MediaWiki\Extension\Translate\Statistics\RebuildMessageGroupStatsJob; |
19 | use MediaWiki\Extension\Translate\SystemUsers\FuzzyBot; |
20 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
21 | use MediaWiki\Html\Html; |
22 | use MediaWiki\Languages\LanguageNameUtils; |
23 | use MediaWiki\Linker\LinkTarget; |
24 | use MediaWiki\Logger\LoggerFactory; |
25 | use MediaWiki\MainConfigNames; |
26 | use MediaWiki\MediaWikiServices; |
27 | use MediaWiki\Page\PageIdentity; |
28 | use MediaWiki\Revision\MutableRevisionRecord; |
29 | use MediaWiki\Revision\RenderedRevision; |
30 | use MediaWiki\Revision\RevisionRecord; |
31 | use MediaWiki\Revision\SlotRecord; |
32 | use MediaWiki\Storage\EditResult; |
33 | use MediaWiki\StubObject\StubUserLang; |
34 | use MediaWiki\Title\Title; |
35 | use MediaWiki\User\UserIdentity; |
36 | use MessageGroupStats; |
37 | use ObjectCache; |
38 | use OutputPage; |
39 | use Parser; |
40 | use ParserOutput; |
41 | use PPFrame; |
42 | use RequestContext; |
43 | use Skin; |
44 | use SpecialPage; |
45 | use Status; |
46 | use TextContent; |
47 | use User; |
48 | use UserBlockedError; |
49 | use Wikimedia\ScopedCallback; |
50 | use WikiPage; |
51 | use WikiPageMessageGroup; |
52 | use WikitextContent; |
53 | |
54 | /** |
55 | * Hooks for page translation. |
56 | * @author Niklas Laxström |
57 | * @license GPL-2.0-or-later |
58 | * @ingroup PageTranslation |
59 | */ |
60 | class Hooks { |
61 | private const PAGEPROP_HAS_LANGUAGES_TAG = 'translate-has-languages-tag'; |
62 | // Uuugly hacks |
63 | public static $allowTargetEdit = false; |
64 | /** State flag used by DeleteTranslatableBundleJob for performance optimizations. */ |
65 | public static bool $isDeleteTranslatableBundleJobRunning = false; |
66 | // Check if we are just rendering tags or such |
67 | public static $renderingContext = false; |
68 | // Used to communicate data between LanguageLinks and SkinTemplateGetLanguageLink hooks. |
69 | private static $languageLinkData = []; |
70 | |
71 | /** |
72 | * Hook: ParserBeforeInternalParse |
73 | * @param Parser $wikitextParser |
74 | * @param null|string &$text |
75 | * @param-taint $text escapes_htmlnoent |
76 | * @param mixed $state |
77 | */ |
78 | public static function renderTagPage( $wikitextParser, &$text, $state ): void { |
79 | if ( $text === null ) { |
80 | // SMW is unhelpfully sending null text if the source contains section tags. Do not explode. |
81 | return; |
82 | } |
83 | |
84 | self::preprocessTagPage( $wikitextParser, $text, $state ); |
85 | |
86 | // Skip further interface message parsing |
87 | if ( $wikitextParser->getOptions()->getInterfaceMessage() ) { |
88 | return; |
89 | } |
90 | |
91 | // For section previews, perform additional clean-up, given tags are often |
92 | // unbalanced when we preview one section only. |
93 | if ( $wikitextParser->getOptions()->getIsSectionPreview() ) { |
94 | $translatablePageParser = Services::getInstance()->getTranslatablePageParser(); |
95 | $text = $translatablePageParser->cleanupTags( $text ); |
96 | } |
97 | |
98 | // Set display title |
99 | $title = MediaWikiServices::getInstance() |
100 | ->getTitleFactory() |
101 | ->castFromPageReference( $wikitextParser->getPage() ); |
102 | if ( !$title ) { |
103 | return; |
104 | } |
105 | |
106 | $page = TranslatablePage::isTranslationPage( $title ); |
107 | if ( !$page ) { |
108 | return; |
109 | } |
110 | |
111 | $wikitextParser->getOutput()->setPageProperty( 'translate-is-translation', true ); |
112 | |
113 | try { |
114 | self::$renderingContext = true; |
115 | [ , $code ] = Utilities::figureMessage( $title->getText() ); |
116 | $name = $page->getPageDisplayTitle( $code ); |
117 | if ( $name ) { |
118 | $name = $wikitextParser->recursivePreprocess( $name ); |
119 | |
120 | $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory() |
121 | ->getLanguageConverter( $wikitextParser->getTargetLanguage() ); |
122 | $name = $langConv->convert( $name ); |
123 | $wikitextParser->getOutput()->setDisplayTitle( $name ); |
124 | } |
125 | self::$renderingContext = false; |
126 | } catch ( Exception $e ) { |
127 | LoggerFactory::getInstance( 'Translate' )->error( |
128 | 'T302754 Failed to set display title for page {title}', |
129 | [ |
130 | 'title' => $title->getPrefixedDBkey(), |
131 | 'text' => $text, |
132 | 'pageid' => $title->getId(), |
133 | ] |
134 | ); |
135 | |
136 | // Re-throw to preserve behavior |
137 | throw $e; |
138 | } |
139 | |
140 | $extensionData = [ |
141 | 'languagecode' => $code, |
142 | 'messagegroupid' => $page->getMessageGroupId(), |
143 | 'sourcepagetitle' => [ |
144 | 'namespace' => $page->getTitle()->getNamespace(), |
145 | 'dbkey' => $page->getTitle()->getDBkey() |
146 | ] |
147 | ]; |
148 | |
149 | $wikitextParser->getOutput()->setExtensionData( 'translate-translation-page', $extensionData ); |
150 | // Disable edit section links |
151 | $wikitextParser->getOutput()->setExtensionData( 'Translate-noeditsection', true ); |
152 | } |
153 | |
154 | /** |
155 | * Hook: ParserBeforePreprocess |
156 | * @param Parser $wikitextParser |
157 | * @param string &$text |
158 | * @param-taint $text escapes_htmlnoent |
159 | * @param mixed $state |
160 | */ |
161 | public static function preprocessTagPage( $wikitextParser, &$text, $state ): void { |
162 | $translatablePageParser = Services::getInstance()->getTranslatablePageParser(); |
163 | |
164 | if ( $translatablePageParser->containsMarkup( $text ) ) { |
165 | try { |
166 | $parserOutput = $translatablePageParser->parse( $text ); |
167 | // If parsing succeeds, replace text and add styles |
168 | $text = $parserOutput->sourcePageTextForRendering( |
169 | $wikitextParser->getTargetLanguage() |
170 | ); |
171 | $wikitextParser->getOutput()->addModuleStyles( [ |
172 | 'ext.translate', |
173 | ] ); |
174 | } catch ( ParsingFailure $e ) { |
175 | wfDebug( 'ParsingFailure caught; expected' ); |
176 | } |
177 | } else { |
178 | // If the text doesn't contain <translate> markup, it can still contain <tvar> in the |
179 | // context of a Parsoid template expansion sub-pipeline. We strip these as well. |
180 | $unit = new TranslationUnit( $text ); |
181 | $text = $unit->getTextForTrans(); |
182 | } |
183 | } |
184 | |
185 | /** |
186 | * Hook: ParserOutputPostCacheTransform |
187 | * @param ParserOutput $out |
188 | * @param string &$text |
189 | * @param array &$options |
190 | */ |
191 | public static function onParserOutputPostCacheTransform( |
192 | ParserOutput $out, |
193 | &$text, |
194 | array &$options |
195 | ) { |
196 | if ( $out->getExtensionData( 'Translate-noeditsection' ) ) { |
197 | $options['enableSectionEditLinks'] = false; |
198 | } |
199 | } |
200 | |
201 | /** |
202 | * This sets &$revRecord to the revision of transcluded page translation if it exists, |
203 | * or sets it to the source language if the page translation does not exist. |
204 | * The page translation is chosen based on language of the source page. |
205 | * |
206 | * Hook: BeforeParserFetchTemplateRevisionRecord |
207 | * @param LinkTarget|null $contextLink |
208 | * @param LinkTarget|null $templateLink |
209 | * @param bool &$skip |
210 | * @param RevisionRecord|null &$revRecord |
211 | */ |
212 | public static function fetchTranslatableTemplateAndTitle( |
213 | ?LinkTarget $contextLink, |
214 | ?LinkTarget $templateLink, |
215 | bool &$skip, |
216 | ?RevisionRecord &$revRecord |
217 | ): void { |
218 | if ( !$templateLink ) { |
219 | return; |
220 | } |
221 | |
222 | $templateTitle = Title::castFromLinkTarget( $templateLink ); |
223 | |
224 | $templateTranslationPage = TranslatablePage::isTranslationPage( $templateTitle ); |
225 | if ( $templateTranslationPage ) { |
226 | // Template is referring to a translation page, fetch it and incase it doesn't |
227 | // exist, fetch the source fallback. |
228 | $revRecord = $templateTranslationPage->getRevisionRecordWithFallback(); |
229 | if ( !$revRecord ) { |
230 | // In rare cases fetching of the source fallback might fail. See: T323863 |
231 | LoggerFactory::getInstance( 'Translate' )->warning( |
232 | "T323863: Could not fetch any revision record for '{groupid}'", |
233 | [ 'groupid' => $templateTranslationPage->getMessageGroupId() ] |
234 | ); |
235 | } |
236 | return; |
237 | } |
238 | |
239 | if ( !TranslatablePage::isSourcePage( $templateTitle ) ) { |
240 | return; |
241 | } |
242 | |
243 | $translatableTemplatePage = TranslatablePage::newFromTitle( $templateTitle ); |
244 | |
245 | if ( !( $translatableTemplatePage->supportsTransclusion() ?? false ) ) { |
246 | // Page being transcluded does not support language aware transclusion |
247 | return; |
248 | } |
249 | |
250 | $store = MediaWikiServices::getInstance()->getRevisionStore(); |
251 | |
252 | if ( $contextLink ) { |
253 | // Fetch the context page language, and then check if template is present in that language |
254 | $templateTranslationTitle = $templateTitle->getSubpage( |
255 | Title::castFromLinkTarget( $contextLink )->getPageLanguage()->getCode() |
256 | ); |
257 | |
258 | if ( $templateTranslationTitle ) { |
259 | if ( $templateTranslationTitle->exists() ) { |
260 | // Template is present in the context page language, fetch the revision record and return |
261 | $revRecord = $store->getRevisionByTitle( $templateTranslationTitle ); |
262 | } else { |
263 | // In case the template has not been translated to the context page language, |
264 | // we assign a MutableRevisionRecord in order to add a dependency, so that when |
265 | // it is created, the newly created page is loaded rather than the fallback |
266 | $revRecord = new MutableRevisionRecord( $templateTranslationTitle ); |
267 | } |
268 | return; |
269 | } |
270 | } |
271 | |
272 | // Context page information not available OR the template translation title could not be determined. |
273 | // Fetch and return the RevisionRecord of the template in the source language |
274 | $sourceTemplateTitle = $templateTitle->getSubpage( |
275 | $translatableTemplatePage->getMessageGroup()->getSourceLanguage() |
276 | ); |
277 | if ( $sourceTemplateTitle && $sourceTemplateTitle->exists() ) { |
278 | $revRecord = $store->getRevisionByTitle( $sourceTemplateTitle ); |
279 | } |
280 | } |
281 | |
282 | /** |
283 | * Set the right page content language for translated pages ("Page/xx"). |
284 | * Hook: PageContentLanguage |
285 | * @param Title $title |
286 | * @param Language|StubUserLang|string &$pageLang |
287 | */ |
288 | public static function onPageContentLanguage( Title $title, &$pageLang ) { |
289 | // For translation pages, parse plural, grammar etc. with correct language, |
290 | // and set the right direction |
291 | if ( TranslatablePage::isTranslationPage( $title ) ) { |
292 | [ , $code ] = Utilities::figureMessage( $title->getText() ); |
293 | $pageLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code ); |
294 | } |
295 | } |
296 | |
297 | /** |
298 | * Display an edit notice for translatable source pages if it's enabled |
299 | * Hook: TitleGetEditNotices |
300 | * @param Title $title |
301 | * @param int $oldid |
302 | * @param array &$notices |
303 | */ |
304 | public static function onTitleGetEditNotices( Title $title, int $oldid, array &$notices ) { |
305 | if ( TranslatablePage::isSourcePage( $title ) ) { |
306 | $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage(); |
307 | if ( !$msg->isDisabled() ) { |
308 | $notices['translate-tag'] = $msg->parseAsBlock(); |
309 | } |
310 | |
311 | $notices[] = Html::warningBox( |
312 | wfMessage( 'tps-edit-sourcepage-text' )->parse(), |
313 | 'translate-edit-documentation' |
314 | ); |
315 | |
316 | // The check is "we're using visual editor for WYSIWYG" (as opposed to "for wikitext |
317 | // edition") - the message will not be displayed in that case. |
318 | $request = RequestContext::getMain()->getRequest(); |
319 | if ( $request->getVal( 'action' ) === 'visualeditor' && |
320 | $request->getVal( 'paction' ) !== 'wikitext' |
321 | ) { |
322 | $notices[] = Html::warningBox( |
323 | wfMessage( 'tps-edit-sourcepage-ve-warning-limited-text' )->parse(), |
324 | 'translate-edit-documentation' |
325 | ); |
326 | } |
327 | } |
328 | } |
329 | |
330 | /** |
331 | * Hook: BeforePageDisplay |
332 | * @param OutputPage $out |
333 | * @param Skin $skin |
334 | */ |
335 | public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) { |
336 | global $wgTranslatePageTranslationULS; |
337 | |
338 | $title = $out->getTitle(); |
339 | $isSource = TranslatablePage::isSourcePage( $title ); |
340 | $isTranslation = TranslatablePage::isTranslationPage( $title ); |
341 | |
342 | if ( $isSource || $isTranslation ) { |
343 | if ( $wgTranslatePageTranslationULS ) { |
344 | $out->addModules( 'ext.translate.pagetranslation.uls' ); |
345 | } |
346 | |
347 | if ( $isSource ) { |
348 | // Adding a help notice |
349 | $out->addModuleStyles( 'ext.translate.edit.documentation.styles' ); |
350 | } |
351 | |
352 | if ( $isTranslation ) { |
353 | // Source pages get this module via <translate>, but for translation |
354 | // pages we need to add it manually. |
355 | $out->addModuleStyles( 'ext.translate' ); |
356 | } |
357 | |
358 | $out->addJsConfigVars( 'wgTranslatePageTranslation', $isTranslation ? 'translation' : 'source' ); |
359 | } |
360 | } |
361 | |
362 | /** |
363 | * Hook: onVisualEditorBeforeEditor |
364 | * @param OutputPage $out |
365 | * @param Skin $skin |
366 | * @return bool |
367 | */ |
368 | public static function onVisualEditorBeforeEditor( OutputPage $out, Skin $skin ) { |
369 | return !TranslatablePage::isTranslationPage( $out->getTitle() ); |
370 | } |
371 | |
372 | /** |
373 | * This is triggered after an edit to translation unit page |
374 | * @param WikiPage $wikiPage |
375 | * @param User $user |
376 | * @param TextContent $content |
377 | * @param string $summary |
378 | * @param bool $minor |
379 | * @param int $flags |
380 | * @param MessageHandle $handle |
381 | */ |
382 | public static function onSectionSave( |
383 | WikiPage $wikiPage, |
384 | User $user, |
385 | TextContent $content, |
386 | $summary, |
387 | $minor, |
388 | $flags, |
389 | MessageHandle $handle |
390 | ) { |
391 | // FuzzyBot may do some duplicate work already worked on by other jobs |
392 | if ( $user->equals( FuzzyBot::getUser() ) ) { |
393 | return; |
394 | } |
395 | |
396 | $group = $handle->getGroup(); |
397 | if ( !$group instanceof WikiPageMessageGroup ) { |
398 | return; |
399 | } |
400 | |
401 | // Finally we know the title and can construct a Translatable page |
402 | $page = TranslatablePage::newFromTitle( $group->getTitle() ); |
403 | |
404 | // Update the target translation page |
405 | if ( !$handle->isDoc() ) { |
406 | $code = $handle->getCode(); |
407 | DeferredUpdates::addCallableUpdate( |
408 | function () use ( $page, $code, $user, $flags, $summary, $handle ) { |
409 | $unitTitle = $handle->getTitle(); |
410 | self::updateTranslationPage( $page, $code, $user, $flags, $summary, null, $unitTitle ); |
411 | } |
412 | ); |
413 | } |
414 | } |
415 | |
416 | private static function updateTranslationPage( |
417 | TranslatablePage $page, |
418 | string $code, |
419 | User $user, |
420 | int $flags, |
421 | string $summary, |
422 | ?string $triggerAction = null, |
423 | ?Title $unitTitle = null |
424 | ): void { |
425 | $source = $page->getTitle(); |
426 | $target = $source->getSubpage( $code ); |
427 | $mwInstance = MediaWikiServices::getInstance(); |
428 | |
429 | // We don't know and don't care |
430 | $flags &= ~EDIT_NEW & ~EDIT_UPDATE; |
431 | |
432 | // Update the target page |
433 | $unitTitleText = $unitTitle ? $unitTitle->getPrefixedText() : null; |
434 | $job = RenderTranslationPageJob::newJob( $target, $triggerAction, $unitTitleText ); |
435 | $job->setUser( $user ); |
436 | $job->setSummary( $summary ); |
437 | $job->setFlags( $flags ); |
438 | $mwInstance->getJobQueueGroup()->push( $job ); |
439 | |
440 | // Invalidate caches so that language bar is up-to-date |
441 | $pages = $page->getTranslationPages(); |
442 | $wikiPageFactory = $mwInstance->getWikiPageFactory(); |
443 | foreach ( $pages as $title ) { |
444 | if ( $title->equals( $target ) ) { |
445 | // Handled by the RenderTranslationPageJob |
446 | continue; |
447 | } |
448 | |
449 | $wikiPage = $wikiPageFactory->newFromTitle( $title ); |
450 | $wikiPage->doPurge(); |
451 | } |
452 | $sourceWikiPage = $wikiPageFactory->newFromTitle( $source ); |
453 | $sourceWikiPage->doPurge(); |
454 | } |
455 | |
456 | /** |
457 | * Hook: GetMagicVariableIDs |
458 | * @param string[] &$variableIDs |
459 | */ |
460 | public static function onGetMagicVariableIDs( &$variableIDs ): void { |
461 | $variableIDs[] = 'translatablepage'; |
462 | } |
463 | |
464 | /** |
465 | * Hook: ParserGetVariableValueSwitch |
466 | */ |
467 | public static function onParserGetVariableValueSwitch( |
468 | Parser $parser, |
469 | array &$variableCache, |
470 | string $magicWordId, |
471 | ?string &$ret, |
472 | PPFrame $frame |
473 | ): void { |
474 | switch ( $magicWordId ) { |
475 | case 'translatablepage': |
476 | $title = Title::castFromPageReference( $parser->getPage() ); |
477 | $pageStatus = self::getTranslatablePageStatus( $title ); |
478 | $ret = $pageStatus !== null ? $pageStatus['page']->getTitle()->getPrefixedText() : ''; |
479 | $variableCache[$magicWordId] = $ret; |
480 | break; |
481 | } |
482 | } |
483 | |
484 | /** |
485 | * @param string $data |
486 | * @param array $params |
487 | * @param Parser $parser |
488 | * @return string |
489 | */ |
490 | public static function languages( $data, $params, $parser ) { |
491 | global $wgPageTranslationLanguageList; |
492 | |
493 | if ( $wgPageTranslationLanguageList === 'sidebar-only' ) { |
494 | return ''; |
495 | } |
496 | |
497 | self::$renderingContext = true; |
498 | $context = new ScopedCallback( static function () { |
499 | self::$renderingContext = false; |
500 | } ); |
501 | |
502 | // Store a property that we can avoid adding language links when |
503 | // $wgPageTranslationLanguageList === 'sidebar-fallback' |
504 | $parser->getOutput()->setPageProperty( self::PAGEPROP_HAS_LANGUAGES_TAG, true ); |
505 | |
506 | $currentTitle = $parser->getTitle(); |
507 | $pageStatus = self::getTranslatablePageStatus( $currentTitle ); |
508 | if ( !$pageStatus ) { |
509 | return ''; |
510 | } |
511 | |
512 | $page = $pageStatus[ 'page' ]; |
513 | $status = $pageStatus[ 'languages' ]; |
514 | $pageTitle = $page->getTitle(); |
515 | |
516 | // Sort by language code, which seems to be the only sane method |
517 | ksort( $status ); |
518 | |
519 | // This way the parser knows to fragment the parser cache by language code |
520 | $userLang = $parser->getOptions()->getUserLangObj(); |
521 | $userLangCode = $userLang->getCode(); |
522 | // Should call $page->getMessageGroup()->getSourceLanguage(), but |
523 | // group is sometimes null on WMF during page moves, reason unknown. |
524 | // This should do the same thing for now. |
525 | $sourceLanguage = $pageTitle->getPageLanguage()->getCode(); |
526 | |
527 | $languages = []; |
528 | $langFactory = MediaWikiServices::getInstance()->getLanguageFactory(); |
529 | foreach ( $status as $code => $percent ) { |
530 | // Get autonyms (null) |
531 | $name = Utilities::getLanguageName( $code, LanguageNameUtils::AUTONYMS ); |
532 | |
533 | // Add links to other languages |
534 | $suffix = ( $code === $sourceLanguage ) ? '' : "/$code"; |
535 | $targetTitleString = $pageTitle->getDBkey() . $suffix; |
536 | $subpage = Title::makeTitle( $pageTitle->getNamespace(), $targetTitleString ); |
537 | |
538 | $classes = []; |
539 | if ( $code === $userLangCode ) { |
540 | $classes[] = 'mw-pt-languages-ui'; |
541 | } |
542 | |
543 | $linker = $parser->getLinkRenderer(); |
544 | $lang = $langFactory->getLanguage( $code ); |
545 | if ( $currentTitle->equals( $subpage ) ) { |
546 | $classes[] = 'mw-pt-languages-selected'; |
547 | $classes = array_merge( $classes, self::tpProgressIcon( (float)$percent ) ); |
548 | $attribs = [ |
549 | 'class' => $classes, |
550 | 'lang' => $lang->getHtmlCode(), |
551 | 'dir' => $lang->getDir(), |
552 | ]; |
553 | |
554 | $contents = Html::element( 'span', $attribs, $name ); |
555 | } elseif ( $subpage->isKnown() ) { |
556 | $pagename = $page->getPageDisplayTitle( $code ); |
557 | if ( !is_string( $pagename ) ) { |
558 | $pagename = $subpage->getPrefixedText(); |
559 | } |
560 | |
561 | $classes = array_merge( $classes, self::tpProgressIcon( (float)$percent ) ); |
562 | |
563 | $title = wfMessage( 'tpt-languages-nonzero' ) |
564 | ->page( $parser->getPage() ) |
565 | ->inLanguage( $userLang ) |
566 | ->params( $pagename ) |
567 | ->numParams( 100 * $percent ) |
568 | ->text(); |
569 | $attribs = [ |
570 | 'title' => $title, |
571 | 'class' => $classes, |
572 | 'lang' => $lang->getHtmlCode(), |
573 | 'dir' => $lang->getDir(), |
574 | ]; |
575 | |
576 | $contents = $linker->makeKnownLink( $subpage, $name, $attribs ); |
577 | } else { |
578 | /* When language is included because it is a priority language, |
579 | * but translations don't exist link directly to the |
580 | * translation view. */ |
581 | $specialTranslateTitle = SpecialPage::getTitleFor( 'Translate' ); |
582 | $params = [ |
583 | 'group' => $page->getMessageGroupId(), |
584 | 'language' => $code, |
585 | 'task' => 'view' |
586 | ]; |
587 | |
588 | $classes[] = 'new'; // For red link color |
589 | |
590 | $attribs = [ |
591 | 'title' => wfMessage( 'tpt-languages-zero' ) |
592 | ->page( $parser->getPage() ) |
593 | ->inLanguage( $userLang ) |
594 | ->text(), |
595 | 'class' => $classes, |
596 | 'lang' => $lang->getHtmlCode(), |
597 | 'dir' => $lang->getDir(), |
598 | ]; |
599 | $contents = $linker->makeKnownLink( $specialTranslateTitle, $name, $attribs, $params ); |
600 | } |
601 | $languages[ $name ] = Html::rawElement( 'li', [], $contents ); |
602 | } |
603 | |
604 | // Sort languages by autonym |
605 | ksort( $languages ); |
606 | $languages = array_values( $languages ); |
607 | $languages = implode( "\n", $languages ); |
608 | |
609 | $out = Html::openElement( 'div', [ |
610 | 'class' => 'mw-pt-languages noprint navigation-not-searchable', |
611 | 'lang' => $userLang->getHtmlCode(), |
612 | 'dir' => $userLang->getDir() |
613 | ] ); |
614 | $out .= Html::rawElement( 'div', [ 'class' => 'mw-pt-languages-label' ], |
615 | wfMessage( 'tpt-languages-legend' ) |
616 | ->page( $parser->getPage() ) |
617 | ->inLanguage( $userLang ) |
618 | ->escaped() |
619 | ); |
620 | $out .= Html::rawElement( |
621 | 'ul', |
622 | [ 'class' => 'mw-pt-languages-list' ], |
623 | $languages |
624 | ); |
625 | $out .= Html::closeElement( 'div' ); |
626 | |
627 | $parser->getOutput()->addModuleStyles( [ |
628 | 'ext.translate.tag.languages', |
629 | ] ); |
630 | |
631 | return $out; |
632 | } |
633 | |
634 | /** |
635 | * Return icon CSS class for given progress status: percentages |
636 | * are too accurate and take more space than simple images. |
637 | * @param float $percent |
638 | * @return string[] |
639 | */ |
640 | private static function tpProgressIcon( float $percent ) { |
641 | $classes = [ 'mw-pt-progress' ]; |
642 | $percent *= 100; |
643 | if ( $percent < 15 ) { |
644 | $classes[] = 'mw-pt-progress--low'; |
645 | } elseif ( $percent < 70 ) { |
646 | $classes[] = 'mw-pt-progress--med'; |
647 | } elseif ( $percent < 100 ) { |
648 | $classes[] = 'mw-pt-progress--high'; |
649 | } else { |
650 | $classes[] = 'mw-pt-progress--complete'; |
651 | } |
652 | return $classes; |
653 | } |
654 | |
655 | /** |
656 | * Returns translatable page and language stats for given title. |
657 | * @return array{page:TranslatablePage,languages:array}|null Returns null if not a translatable page. |
658 | */ |
659 | private static function getTranslatablePageStatus( ?Title $title ): ?array { |
660 | if ( $title === null ) { |
661 | return null; |
662 | } |
663 | // Check if this is a source page or a translation page |
664 | $page = TranslatablePage::newFromTitle( $title ); |
665 | if ( $page->getMarkedTag() === null ) { |
666 | $page = TranslatablePage::isTranslationPage( $title ); |
667 | } |
668 | |
669 | if ( $page === false || $page->getMarkedTag() === null ) { |
670 | return null; |
671 | } |
672 | |
673 | $status = $page->getTranslationPercentages(); |
674 | if ( !$status ) { |
675 | return null; |
676 | } |
677 | |
678 | $messageGroupMetadata = Services::getInstance()->getMessageGroupMetadata(); |
679 | // If priority languages have been set, always show those languages |
680 | $priorityLanguages = $messageGroupMetadata->get( $page->getMessageGroupId(), 'prioritylangs' ); |
681 | if ( (string)$priorityLanguages !== '' ) { |
682 | $status += array_fill_keys( explode( ',', $priorityLanguages ), 0 ); |
683 | } |
684 | |
685 | return [ |
686 | 'page' => $page, |
687 | 'languages' => $status |
688 | ]; |
689 | } |
690 | |
691 | /** |
692 | * Hooks: LanguageLinks |
693 | * @param Title $title Title of the page for which links are needed. |
694 | * @param array &$languageLinks List of language links to modify. |
695 | */ |
696 | public static function addLanguageLinks( Title $title, array &$languageLinks ) { |
697 | global $wgPageTranslationLanguageList; |
698 | |
699 | if ( $wgPageTranslationLanguageList === 'tag-only' ) { |
700 | return; |
701 | } |
702 | |
703 | if ( $wgPageTranslationLanguageList === 'sidebar-fallback' ) { |
704 | $pageProps = MediaWikiServices::getInstance()->getPageProps(); |
705 | $languageProp = $pageProps->getProperties( $title, self::PAGEPROP_HAS_LANGUAGES_TAG ); |
706 | if ( $languageProp !== [] ) { |
707 | return; |
708 | } |
709 | } |
710 | |
711 | // $wgPageTranslationLanguageList === 'sidebar-always' OR 'sidebar-only' |
712 | |
713 | $status = self::getTranslatablePageStatus( $title ); |
714 | if ( !$status ) { |
715 | return; |
716 | } |
717 | |
718 | self::$renderingContext = true; |
719 | $context = new ScopedCallback( static function () { |
720 | self::$renderingContext = false; |
721 | } ); |
722 | |
723 | $page = $status[ 'page' ]; |
724 | $languages = $status[ 'languages' ]; |
725 | $mwServices = MediaWikiServices::getInstance(); |
726 | $en = $mwServices->getLanguageFactory()->getLanguage( 'en' ); |
727 | |
728 | $newLanguageLinks = []; |
729 | |
730 | // Batch the Title::exists queries used below |
731 | $lb = $mwServices->getLinkBatchFactory()->newLinkBatch(); |
732 | foreach ( array_keys( $languages ) as $code ) { |
733 | $title = $page->getTitle()->getSubpage( $code ); |
734 | $lb->addObj( $title ); |
735 | } |
736 | $lb->execute(); |
737 | $languageNameUtils = $mwServices->getLanguageNameUtils(); |
738 | foreach ( $languages as $code => $percentage ) { |
739 | $title = $page->getTitle()->getSubpage( $code ); |
740 | $key = "x-pagetranslation:{$title->getPrefixedText()}"; |
741 | $translatedName = $page->getPageDisplayTitle( $code ) ?: $title->getPrefixedText(); |
742 | |
743 | if ( $title->exists() ) { |
744 | $href = $title->getLocalURL(); |
745 | $classes = self::tpProgressIcon( (float)$percentage ); |
746 | $title = wfMessage( 'tpt-languages-nonzero' ) |
747 | ->params( $translatedName ) |
748 | ->numParams( 100 * $percentage ); |
749 | } else { |
750 | $href = SpecialPage::getTitleFor( 'Translate' )->getLocalURL( [ |
751 | 'group' => $page->getMessageGroupId(), |
752 | 'language' => $code, |
753 | ] ); |
754 | $classes = [ 'mw-pt-progress--none' ]; |
755 | $title = wfMessage( 'tpt-languages-zero' ); |
756 | } |
757 | |
758 | self::$languageLinkData[ $key ] = [ |
759 | 'href' => $href, |
760 | 'language' => $code, |
761 | 'percentage' => $percentage, |
762 | 'classes' => $classes, |
763 | 'autonym' => $en->ucfirst( $languageNameUtils->getLanguageName( $code ) ), |
764 | 'title' => $title, |
765 | ]; |
766 | |
767 | $newLanguageLinks[ $key ] = self::$languageLinkData[ $key ][ 'autonym' ]; |
768 | } |
769 | |
770 | asort( $newLanguageLinks ); |
771 | $languageLinks = array_merge( array_keys( $newLanguageLinks ), $languageLinks ); |
772 | } |
773 | |
774 | /** |
775 | * Hooks: SkinTemplateGetLanguageLink |
776 | * @param array &$link |
777 | * @param Title $linkTitle |
778 | * @param Title $pageTitle |
779 | * @param OutputPage $out |
780 | */ |
781 | public static function formatLanguageLink( |
782 | array &$link, |
783 | Title $linkTitle, |
784 | Title $pageTitle, |
785 | OutputPage $out |
786 | ) { |
787 | if ( !str_starts_with( $link['text'], 'x-pagetranslation:' ) || |
788 | !isset( self::$languageLinkData[ $link['text'] ] ) |
789 | ) { |
790 | return; |
791 | } |
792 | |
793 | $data = self::$languageLinkData[ $link[ 'text' ] ]; |
794 | |
795 | $link[ 'class' ] .= ' ' . implode( ' ', $data[ 'classes' ] ); |
796 | $link[ 'href' ] = $data[ 'href' ]; |
797 | $link[ 'text' ] = $data[ 'autonym' ]; |
798 | $link[ 'title' ] = $data[ 'title' ]->inLanguage( $out->getLanguage()->getCode() )->text(); |
799 | $link[ 'lang'] = LanguageCode::bcp47( $data[ 'language' ] ); |
800 | $link[ 'hreflang'] = LanguageCode::bcp47( $data[ 'language' ] ); |
801 | |
802 | $out->addModuleStyles( 'ext.translate.tag.languages' ); |
803 | } |
804 | |
805 | /** |
806 | * Display nice error when editing content. |
807 | * Hook: EditFilterMergedContent |
808 | * @param IContextSource $context |
809 | * @param Content $content |
810 | * @param Status $status |
811 | * @param string $summary |
812 | * @return bool |
813 | */ |
814 | public static function tpSyntaxCheckForEditContent( |
815 | $context, |
816 | $content, |
817 | $status, |
818 | $summary |
819 | ) { |
820 | $syntaxErrorStatus = self::tpSyntaxError( $context->getTitle(), $content ); |
821 | |
822 | if ( $syntaxErrorStatus ) { |
823 | $status->merge( $syntaxErrorStatus ); |
824 | return $syntaxErrorStatus->isGood(); |
825 | } |
826 | |
827 | return true; |
828 | } |
829 | |
830 | private static function tpSyntaxError( ?PageIdentity $page, ?Content $content ): ?Status { |
831 | // T163254: Ignore translation markup on non-wikitext pages |
832 | if ( !$content instanceof WikitextContent || !$page ) { |
833 | return null; |
834 | } |
835 | |
836 | $text = $content->getText(); |
837 | |
838 | // See T154500 |
839 | $text = TextContent::normalizeLineEndings( $text ); |
840 | $status = Status::newGood(); |
841 | $parser = Services::getInstance()->getTranslatablePageParser(); |
842 | if ( $parser->containsMarkup( $text ) ) { |
843 | try { |
844 | $parser->parse( $text ); |
845 | } catch ( ParsingFailure $e ) { |
846 | $status->fatal( ...( $e->getMessageSpecification() ) ); |
847 | } |
848 | } |
849 | |
850 | return $status; |
851 | } |
852 | |
853 | /** |
854 | * When attempting to save, last resort. Edit page would only display |
855 | * edit conflict if there wasn't tpSyntaxCheckForEditPage. |
856 | * Hook: MultiContentSave |
857 | * @param RenderedRevision $renderedRevision |
858 | * @param UserIdentity $user |
859 | * @param CommentStoreComment $summary |
860 | * @param int $flags |
861 | * @param Status $hookStatus |
862 | * @return bool |
863 | */ |
864 | public static function tpSyntaxCheck( |
865 | RenderedRevision $renderedRevision, |
866 | UserIdentity $user, |
867 | CommentStoreComment $summary, |
868 | $flags, |
869 | Status $hookStatus |
870 | ) { |
871 | $content = $renderedRevision->getRevision()->getContent( SlotRecord::MAIN ); |
872 | |
873 | $status = self::tpSyntaxError( |
874 | $renderedRevision->getRevision()->getPage(), |
875 | $content |
876 | ); |
877 | |
878 | if ( $status ) { |
879 | $hookStatus->merge( $status ); |
880 | return $status->isGood(); |
881 | } |
882 | |
883 | return true; |
884 | } |
885 | |
886 | /** |
887 | * Hook: PageSaveComplete |
888 | * |
889 | * @param WikiPage $wikiPage |
890 | * @param UserIdentity $userIdentity |
891 | * @param string $summary |
892 | * @param int $flags |
893 | * @param RevisionRecord $revisionRecord |
894 | * @param EditResult $editResult |
895 | */ |
896 | public static function addTranstagAfterSave( |
897 | WikiPage $wikiPage, |
898 | UserIdentity $userIdentity, |
899 | string $summary, |
900 | int $flags, |
901 | RevisionRecord $revisionRecord, |
902 | EditResult $editResult |
903 | ) { |
904 | $content = $wikiPage->getContent(); |
905 | |
906 | // T163254: Disable page translation on non-wikitext pages |
907 | if ( $content instanceof WikitextContent ) { |
908 | $text = $content->getText(); |
909 | } else { |
910 | // Not applicable |
911 | return; |
912 | } |
913 | |
914 | $parser = Services::getInstance()->getTranslatablePageParser(); |
915 | if ( $parser->containsMarkup( $text ) ) { |
916 | // Add the ready tag |
917 | $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() ); |
918 | $page->addReadyTag( $revisionRecord->getId() ); |
919 | } |
920 | |
921 | // Schedule a deferred status update for the translatable page. |
922 | $tpStatusUpdater = Services::getInstance()->getTranslatablePageStore(); |
923 | $tpStatusUpdater->performStatusUpdate( $wikiPage->getTitle() ); |
924 | } |
925 | |
926 | /** |
927 | * Page moving and page protection (and possibly other things) creates null |
928 | * revisions. These revisions re-use the previous text already stored in |
929 | * the database. Those however do not trigger re-parsing of the page and |
930 | * thus the ready tag is not updated. This watches for new revisions, |
931 | * checks if they reuse existing text, checks whether the parent version |
932 | * is the latest version and has a ready tag. If that is the case, |
933 | * also adds a ready tag for the new revision (which is safe, because |
934 | * the text hasn't changed). The interface will say that there has been |
935 | * a change, but shows no change in the content. This lets the user to |
936 | * re-mark the translations of the page title as outdated (if enabled |
937 | * for translation). |
938 | * Hook: RevisionRecordInserted |
939 | * @param RevisionRecord $rev |
940 | */ |
941 | public static function updateTranstagOnNullRevisions( RevisionRecord $rev ) { |
942 | $parentId = $rev->getParentId(); |
943 | if ( $parentId === 0 || $parentId === null ) { |
944 | // No parent, bail out. |
945 | return; |
946 | } |
947 | |
948 | $prevRev = MediaWikiServices::getInstance() |
949 | ->getRevisionLookup() |
950 | ->getRevisionById( $parentId ); |
951 | |
952 | if ( !$prevRev || !$rev->hasSameContent( $prevRev ) ) { |
953 | // Not a null revision, bail out. |
954 | return; |
955 | } |
956 | |
957 | $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() ); |
958 | $bundleFactory = Services::getInstance()->getTranslatableBundleFactory(); |
959 | $bundle = $bundleFactory->getBundle( $title ); |
960 | |
961 | if ( $bundle ) { |
962 | $bundleStore = $bundleFactory->getStore( $bundle ); |
963 | $bundleStore->handleNullRevisionInsert( $bundle, $rev ); |
964 | } |
965 | } |
966 | |
967 | /** |
968 | * Prevent creation of orphan translation units in Translations namespace. |
969 | * Hook: getUserPermissionsErrorsExpensive |
970 | * |
971 | * @param Title $title |
972 | * @param User $user |
973 | * @param string $action |
974 | * @param mixed &$result |
975 | * @return bool |
976 | */ |
977 | public static function onGetUserPermissionsErrorsExpensive( |
978 | Title $title, |
979 | User $user, |
980 | $action, |
981 | &$result |
982 | ) { |
983 | $handle = new MessageHandle( $title ); |
984 | |
985 | if ( !$handle->isPageTranslation() || $action === 'read' ) { |
986 | return true; |
987 | } |
988 | |
989 | $isValid = true; |
990 | $groupId = null; |
991 | |
992 | if ( $handle->isValid() ) { |
993 | $group = $handle->getGroup(); |
994 | $groupId = $group->getId(); |
995 | $permissionTitleCheck = null; |
996 | |
997 | if ( $group instanceof WikiPageMessageGroup ) { |
998 | $permissionTitleCheck = $group->getTitle(); |
999 | } elseif ( $group instanceof MessageBundleMessageGroup ) { |
1000 | // TODO: This check for MessageBundle related permission should be in |
1001 | // the MessageBundleTranslation/Hook |
1002 | $permissionTitleCheck = Title::newFromID( $group->getBundlePageId() ); |
1003 | } |
1004 | |
1005 | if ( $permissionTitleCheck ) { |
1006 | // Check for blocks |
1007 | $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); |
1008 | if ( $permissionManager->isBlockedFrom( $user, $permissionTitleCheck ) ) { |
1009 | $block = $user->getBlock(); |
1010 | if ( $block ) { |
1011 | $error = new UserBlockedError( $block, $user ); |
1012 | $errorMessage = $error->getMessageObject(); |
1013 | $result = array_merge( [ $errorMessage->getKey() ], $errorMessage->getParams() ); |
1014 | return false; |
1015 | } |
1016 | } |
1017 | } |
1018 | } |
1019 | |
1020 | // Allow editing units that become orphaned in regular use, so that |
1021 | // people can delete them or fix links or other issues in them. |
1022 | if ( $action !== 'create' ) { |
1023 | return true; |
1024 | } |
1025 | |
1026 | if ( !$handle->isValid() ) { |
1027 | // TODO: These checks may no longer be needed |
1028 | // Sometimes the message index can be out of date. Either the rebuild job failed or |
1029 | // it just hasn't finished yet. Do a secondary check to make sure we are not |
1030 | // inconveniencing translators for no good reason. |
1031 | // See https://phabricator.wikimedia.org/T221119 |
1032 | $statsdDataFactory = MediaWikiServices::getInstance()->getStatsdDataFactory(); |
1033 | $statsdDataFactory->increment( 'translate.slow_translatable_page_check' ); |
1034 | $translatablePage = self::checkTranslatablePageSlow( $title ); |
1035 | if ( $translatablePage ) { |
1036 | $groupId = $translatablePage->getMessageGroupId(); |
1037 | $statsdDataFactory->increment( 'translate.slow_translatable_page_check_valid' ); |
1038 | } else { |
1039 | $isValid = false; |
1040 | } |
1041 | } |
1042 | |
1043 | if ( $isValid ) { |
1044 | $error = self::getTranslationRestrictions( $handle, $groupId ); |
1045 | $result = $error ?: $result; |
1046 | return $error === []; |
1047 | } |
1048 | |
1049 | // Don't allow editing invalid messages that do not belong to any translatable page |
1050 | LoggerFactory::getInstance( 'Translate' )->info( |
1051 | 'Unknown translation page: {title}', |
1052 | [ 'title' => $title->getPrefixedDBkey() ] |
1053 | ); |
1054 | $result = [ 'tpt-unknown-page' ]; |
1055 | return false; |
1056 | } |
1057 | |
1058 | private static function checkTranslatablePageSlow( LinkTarget $unit ): ?TranslatablePage { |
1059 | $parts = TranslatablePage::parseTranslationUnit( $unit ); |
1060 | $translationPageTitle = Title::newFromText( |
1061 | $parts[ 'sourcepage' ] . '/' . $parts[ 'language' ] |
1062 | ); |
1063 | if ( !$translationPageTitle ) { |
1064 | return null; |
1065 | } |
1066 | |
1067 | $translatablePage = TranslatablePage::isTranslationPage( $translationPageTitle ); |
1068 | if ( !$translatablePage ) { |
1069 | return null; |
1070 | } |
1071 | |
1072 | $factory = Services::getInstance()->getTranslationUnitStoreFactory(); |
1073 | $store = $factory->getReader( $translatablePage->getTitle() ); |
1074 | $units = $store->getNames(); |
1075 | |
1076 | if ( !in_array( $parts[ 'section' ], $units ) ) { |
1077 | return null; |
1078 | } |
1079 | |
1080 | return $translatablePage; |
1081 | } |
1082 | |
1083 | /** |
1084 | * Prevent editing of restricted languages when prioritized. |
1085 | * |
1086 | * @param MessageHandle $handle |
1087 | * @param string $groupId |
1088 | * @return array array containing error message if restricted, empty otherwise |
1089 | */ |
1090 | private static function getTranslationRestrictions( MessageHandle $handle, $groupId ) { |
1091 | global $wgTranslateDocumentationLanguageCode; |
1092 | |
1093 | // Allow adding message documentation even when translation is restricted |
1094 | if ( $handle->getCode() === $wgTranslateDocumentationLanguageCode ) { |
1095 | return []; |
1096 | } |
1097 | |
1098 | $messageGroupMetadata = Services::getInstance()->getMessageGroupMetadata(); |
1099 | // Check if anything is prevented for the group in the first place |
1100 | $force = $messageGroupMetadata->get( $groupId, 'priorityforce' ); |
1101 | if ( $force !== 'on' ) { |
1102 | return []; |
1103 | } |
1104 | |
1105 | // And finally check whether the language is in the inclusion list |
1106 | $languages = $messageGroupMetadata->get( $groupId, 'prioritylangs' ); |
1107 | $reason = $messageGroupMetadata->get( $groupId, 'priorityreason' ); |
1108 | if ( !$languages ) { |
1109 | if ( $reason ) { |
1110 | return [ 'tpt-translation-restricted-no-priority-languages', $reason ]; |
1111 | } |
1112 | return [ 'tpt-translation-restricted-no-priority-languages-no-reason' ]; |
1113 | } |
1114 | |
1115 | $filter = array_flip( explode( ',', $languages ) ); |
1116 | if ( !isset( $filter[$handle->getCode()] ) ) { |
1117 | if ( $reason ) { |
1118 | return [ 'tpt-translation-restricted', $reason ]; |
1119 | } |
1120 | |
1121 | return [ 'tpt-translation-restricted-no-reason' ]; |
1122 | } |
1123 | |
1124 | return []; |
1125 | } |
1126 | |
1127 | /** |
1128 | * Prevent editing of translation pages directly. |
1129 | * Hook: getUserPermissionsErrorsExpensive |
1130 | * @param Title $title |
1131 | * @param User $user |
1132 | * @param string $action |
1133 | * @param bool &$result |
1134 | * @return bool |
1135 | */ |
1136 | public static function preventDirectEditing( Title $title, User $user, $action, &$result ) { |
1137 | if ( self::$allowTargetEdit ) { |
1138 | return true; |
1139 | } |
1140 | |
1141 | $inclusionList = [ |
1142 | 'read', 'deletedtext', 'deletedhistory', |
1143 | 'deleterevision', 'suppressrevision', 'viewsuppressed', // T286884 |
1144 | 'review', // FlaggedRevs |
1145 | 'patrol', // T151172 |
1146 | ]; |
1147 | $needsPageTranslationRight = in_array( $action, [ 'delete', 'undelete' ] ); |
1148 | if ( in_array( $action, $inclusionList ) || |
1149 | ( $needsPageTranslationRight && $user->isAllowed( 'pagetranslation' ) ) |
1150 | ) { |
1151 | return true; |
1152 | } |
1153 | |
1154 | $page = TranslatablePage::isTranslationPage( $title ); |
1155 | if ( $page !== false && $page->getMarkedTag() ) { |
1156 | if ( $needsPageTranslationRight ) { |
1157 | $result = User::newFatalPermissionDeniedStatus( 'pagetranslation' )->getMessage(); |
1158 | return false; |
1159 | } |
1160 | |
1161 | [ , $code ] = Utilities::figureMessage( $title->getText() ); |
1162 | $mwService = MediaWikiServices::getInstance(); |
1163 | |
1164 | $translationUrl = $mwService->getUrlUtils()->expand( |
1165 | $page->getTranslationUrl( $code ), PROTO_RELATIVE |
1166 | ); |
1167 | |
1168 | $result = [ |
1169 | 'tpt-target-page', |
1170 | ':' . $page->getTitle()->getPrefixedText(), |
1171 | // This url shouldn't get cached |
1172 | $translationUrl |
1173 | ]; |
1174 | |
1175 | return false; |
1176 | } |
1177 | |
1178 | return true; |
1179 | } |
1180 | |
1181 | /** |
1182 | * Redirects the delete action to our own for translatable pages. |
1183 | * Hook: ArticleConfirmDelete |
1184 | * |
1185 | * @param Article $article |
1186 | * @param OutputPage $out |
1187 | * @param string &$reason |
1188 | * |
1189 | * @return bool |
1190 | */ |
1191 | public static function disableDelete( $article, $out, &$reason ) { |
1192 | $title = $article->getTitle(); |
1193 | $bundle = Services::getInstance()->getTranslatableBundleFactory()->getBundle( $title ); |
1194 | $isDeletableBundle = $bundle && $bundle->isDeletable(); |
1195 | if ( $isDeletableBundle || TranslatablePage::isTranslationPage( $title ) ) { |
1196 | $new = SpecialPage::getTitleFor( |
1197 | 'PageTranslationDeletePage', |
1198 | $title->getPrefixedText() |
1199 | ); |
1200 | $out->redirect( $new->getFullURL() ); |
1201 | } |
1202 | |
1203 | return true; |
1204 | } |
1205 | |
1206 | /** |
1207 | * Hook: ArticleViewHeader |
1208 | * |
1209 | * @param Article $article |
1210 | * @param bool|ParserOutput|null &$outputDone |
1211 | * @param bool &$pcache |
1212 | */ |
1213 | public static function translatablePageHeader( $article, &$outputDone, &$pcache ) { |
1214 | if ( $article->getOldID() ) { |
1215 | return; |
1216 | } |
1217 | |
1218 | $transPage = TranslatablePage::isTranslationPage( $article->getTitle() ); |
1219 | $context = $article->getContext(); |
1220 | if ( $transPage ) { |
1221 | self::translationPageHeader( $context, $transPage ); |
1222 | } else { |
1223 | // Check for pages that are tagged or marked |
1224 | self::sourcePageHeader( $context ); |
1225 | } |
1226 | } |
1227 | |
1228 | private static function sourcePageHeader( IContextSource $context ) { |
1229 | $linker = MediaWikiServices::getInstance()->getLinkRenderer(); |
1230 | |
1231 | $language = $context->getLanguage(); |
1232 | $title = $context->getTitle(); |
1233 | |
1234 | $page = TranslatablePage::newFromTitle( $title ); |
1235 | |
1236 | $marked = $page->getMarkedTag(); |
1237 | $ready = $page->getReadyTag(); |
1238 | $latest = $title->getLatestRevID(); |
1239 | |
1240 | $actions = []; |
1241 | if ( $marked && $context->getUser()->isAllowed( 'translate' ) ) { |
1242 | $actions[] = self::getTranslateLink( $context, $page, null ); |
1243 | } |
1244 | |
1245 | $hasChanges = $ready === $latest && $marked !== $latest; |
1246 | if ( $hasChanges ) { |
1247 | $diffUrl = $title->getFullURL( [ 'oldid' => $marked, 'diff' => $latest ] ); |
1248 | |
1249 | if ( $context->getUser()->isAllowed( 'pagetranslation' ) ) { |
1250 | $pageTranslation = SpecialPage::getTitleFor( 'PageTranslation' ); |
1251 | $params = [ 'target' => $title->getPrefixedText(), 'do' => 'mark' ]; |
1252 | |
1253 | if ( $marked === null ) { |
1254 | // This page has never been marked |
1255 | $linkDesc = $context->msg( 'translate-tag-markthis' )->text(); |
1256 | $actions[] = $linker->makeKnownLink( $pageTranslation, $linkDesc, [], $params ); |
1257 | } else { |
1258 | $markUrl = $pageTranslation->getFullURL( $params ); |
1259 | $actions[] = $context->msg( 'translate-tag-markthisagain', $diffUrl, $markUrl ) |
1260 | ->parse(); |
1261 | } |
1262 | } else { |
1263 | $actions[] = $context->msg( 'translate-tag-hasnew', $diffUrl )->parse(); |
1264 | } |
1265 | } |
1266 | |
1267 | if ( !count( $actions ) ) { |
1268 | return; |
1269 | } |
1270 | |
1271 | $header = Html::rawElement( |
1272 | 'div', |
1273 | [ |
1274 | 'class' => 'mw-pt-translate-header noprint nomobile', |
1275 | 'dir' => $language->getDir(), |
1276 | 'lang' => $language->getHtmlCode(), |
1277 | ], |
1278 | $language->semicolonList( $actions ) |
1279 | ); |
1280 | |
1281 | $context->getOutput()->addHTML( $header ); |
1282 | } |
1283 | |
1284 | private static function getTranslateLink( |
1285 | IContextSource $context, |
1286 | TranslatablePage $page, |
1287 | ?string $langCode |
1288 | ): string { |
1289 | $linker = MediaWikiServices::getInstance()->getLinkRenderer(); |
1290 | |
1291 | return $linker->makeKnownLink( |
1292 | SpecialPage::getTitleFor( 'Translate' ), |
1293 | $context->msg( 'translate-tag-translate-link-desc' )->text(), |
1294 | [], |
1295 | [ |
1296 | 'group' => $page->getMessageGroupId(), |
1297 | 'language' => $langCode, |
1298 | 'action' => 'page', |
1299 | 'filter' => '', |
1300 | ] |
1301 | ); |
1302 | } |
1303 | |
1304 | private static function translationPageHeader( IContextSource $context, TranslatablePage $page ) { |
1305 | global $wgTranslateKeepOutdatedTranslations; |
1306 | |
1307 | $title = $context->getTitle(); |
1308 | if ( !$title->exists() ) { |
1309 | return; |
1310 | } |
1311 | |
1312 | [ , $code ] = Utilities::figureMessage( $title->getText() ); |
1313 | |
1314 | // Get the translation percentage |
1315 | $pers = $page->getTranslationPercentages(); |
1316 | $per = 0; |
1317 | if ( isset( $pers[$code] ) ) { |
1318 | $per = $pers[$code] * 100; |
1319 | } |
1320 | |
1321 | $language = $context->getLanguage(); |
1322 | $output = $context->getOutput(); |
1323 | |
1324 | if ( $page->getSourceLanguageCode() === $code ) { |
1325 | // If we are on the source language page, link to translate for user's language |
1326 | $msg = self::getTranslateLink( $context, $page, $language->getCode() ); |
1327 | } else { |
1328 | $mwService = MediaWikiServices::getInstance(); |
1329 | |
1330 | $translationUrl = $mwService->getUrlUtils()->expand( |
1331 | $page->getTranslationUrl( $code ), PROTO_RELATIVE |
1332 | ); |
1333 | |
1334 | $msg = $context->msg( 'tpt-translation-intro', |
1335 | $translationUrl, |
1336 | ':' . $page->getTitle()->getPrefixedText(), |
1337 | $language->formatNum( $per ) |
1338 | )->parse(); |
1339 | } |
1340 | |
1341 | $header = Html::rawElement( |
1342 | 'div', |
1343 | [ |
1344 | 'class' => 'mw-pt-translate-header noprint', |
1345 | 'dir' => $language->getDir(), |
1346 | 'lang' => $language->getHtmlCode(), |
1347 | ], |
1348 | $msg |
1349 | ); |
1350 | |
1351 | $output->addHTML( $header ); |
1352 | |
1353 | if ( $wgTranslateKeepOutdatedTranslations ) { |
1354 | $groupId = $page->getMessageGroupId(); |
1355 | // This is already calculated and cached by above call to getTranslationPercentages |
1356 | $stats = MessageGroupStats::forItem( $groupId, $code ); |
1357 | if ( $stats[MessageGroupStats::FUZZY] ) { |
1358 | // Only show if there is fuzzy messages |
1359 | $wrap = Html::rawElement( |
1360 | 'div', |
1361 | [ |
1362 | 'class' => 'mw-pt-translate-header', |
1363 | 'dir' => $language->getDir(), |
1364 | 'lang' => $language->getHtmlCode() |
1365 | ], |
1366 | '<span class="mw-translate-fuzzy">$1</span>' |
1367 | ); |
1368 | |
1369 | $output->wrapWikiMsg( $wrap, [ 'tpt-translation-intro-fuzzy' ] ); |
1370 | } |
1371 | } |
1372 | } |
1373 | |
1374 | /** |
1375 | * Hook: SpecialPage_initList |
1376 | * @param array &$list |
1377 | */ |
1378 | public static function replaceMovePage( &$list ) { |
1379 | $movePageSpec = $list['Movepage'] ?? null; |
1380 | |
1381 | // This should never happen, but apparently is happening? See: T296568 |
1382 | if ( $movePageSpec === null ) { |
1383 | return; |
1384 | } |
1385 | |
1386 | $list['Movepage'] = [ |
1387 | 'class' => MoveTranslatableBundleSpecialPage::class, |
1388 | 'services' => [ |
1389 | 'ObjectFactory', |
1390 | 'PermissionManager', |
1391 | 'Translate:TranslatableBundleMover', |
1392 | 'Translate:TranslatableBundleFactory' |
1393 | ], |
1394 | 'args' => [ |
1395 | $movePageSpec |
1396 | ] |
1397 | ]; |
1398 | } |
1399 | |
1400 | /** |
1401 | * Hook: getUserPermissionsErrorsExpensive |
1402 | * @param Title $title |
1403 | * @param User $user |
1404 | * @param string $action |
1405 | * @param mixed &$result |
1406 | * @return bool |
1407 | */ |
1408 | public static function lockedPagesCheck( Title $title, User $user, $action, &$result ) { |
1409 | if ( $action === 'read' ) { |
1410 | return true; |
1411 | } |
1412 | |
1413 | $cache = ObjectCache::getInstance( CACHE_ANYTHING ); |
1414 | $key = $cache->makeKey( 'pt-lock', sha1( $title->getPrefixedText() ) ); |
1415 | if ( $cache->get( $key ) === 'locked' ) { |
1416 | $result = [ 'pt-locked-page' ]; |
1417 | |
1418 | return false; |
1419 | } |
1420 | |
1421 | return true; |
1422 | } |
1423 | |
1424 | /** |
1425 | * Hook: SkinSubPageSubtitle |
1426 | * @param array &$subpages |
1427 | * @param ?Skin $skin |
1428 | * @param OutputPage $out |
1429 | * @return bool |
1430 | */ |
1431 | public static function replaceSubtitle( &$subpages, ?Skin $skin, OutputPage $out ) { |
1432 | $linker = MediaWikiServices::getInstance()->getLinkRenderer(); |
1433 | |
1434 | $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() ); |
1435 | if ( !$isTranslationPage |
1436 | && !TranslatablePage::isSourcePage( $out->getTitle() ) |
1437 | ) { |
1438 | return true; |
1439 | } |
1440 | |
1441 | // Copied from Skin::subPageSubtitle() |
1442 | $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); |
1443 | if ( |
1444 | $out->isArticle() && |
1445 | $nsInfo->hasSubpages( $out->getTitle()->getNamespace() ) |
1446 | ) { |
1447 | $ptext = $out->getTitle()->getPrefixedText(); |
1448 | $links = explode( '/', $ptext ); |
1449 | if ( count( $links ) > 1 ) { |
1450 | array_pop( $links ); |
1451 | if ( $isTranslationPage ) { |
1452 | // Also remove language code page |
1453 | array_pop( $links ); |
1454 | } |
1455 | $c = 0; |
1456 | $growinglink = ''; |
1457 | $display = ''; |
1458 | $lang = $skin->getLanguage(); |
1459 | |
1460 | foreach ( $links as $link ) { |
1461 | $growinglink .= $link; |
1462 | $display .= $link; |
1463 | $linkObj = Title::newFromText( $growinglink ); |
1464 | |
1465 | if ( $linkObj && $linkObj->isKnown() ) { |
1466 | $getlink = $linker->makeKnownLink( |
1467 | SpecialPage::getTitleFor( 'MyLanguage', $growinglink ), |
1468 | $display |
1469 | ); |
1470 | |
1471 | $c++; |
1472 | |
1473 | if ( $c > 1 ) { |
1474 | $subpages .= $lang->getDirMarkEntity() . $skin->msg( 'pipe-separator' )->escaped(); |
1475 | } else { |
1476 | $subpages .= '< '; |
1477 | } |
1478 | |
1479 | $subpages .= $getlink; |
1480 | $display = ''; |
1481 | } else { |
1482 | $display .= '/'; |
1483 | } |
1484 | |
1485 | $growinglink .= '/'; |
1486 | } |
1487 | } |
1488 | |
1489 | return false; |
1490 | } |
1491 | |
1492 | return true; |
1493 | } |
1494 | |
1495 | /** |
1496 | * Converts the edit tab (if exists) for translation pages to translate tab. |
1497 | * Hook: SkinTemplateNavigation::Universal |
1498 | * @param Skin $skin |
1499 | * @param array &$tabs |
1500 | */ |
1501 | public static function translateTab( Skin $skin, array &$tabs ) { |
1502 | $title = $skin->getTitle(); |
1503 | $handle = new MessageHandle( $title ); |
1504 | $code = $handle->getCode(); |
1505 | $page = TranslatablePage::isTranslationPage( $title ); |
1506 | // The source language has a subpage too, but cannot be translated |
1507 | if ( !$page || $page->getSourceLanguageCode() === $code ) { |
1508 | return; |
1509 | } |
1510 | |
1511 | $user = $skin->getUser(); |
1512 | if ( isset( $tabs['views']['edit'] ) ) { |
1513 | // There is an edit tab, just replace its text and URL with ours, keeping the tooltip and access key |
1514 | $tabs['views']['edit']['text'] = $skin->msg( 'tpt-tab-translate' )->text(); |
1515 | $tabs['views']['edit']['href'] = $page->getTranslationUrl( $code ); |
1516 | } elseif ( $user->isAllowed( 'translate' ) ) { |
1517 | $mwInstance = MediaWikiServices::getInstance(); |
1518 | $namespaceProtection = $mwInstance->getMainConfig()->get( MainConfigNames::NamespaceProtection ); |
1519 | $permissionManager = $mwInstance->getPermissionManager(); |
1520 | if ( |
1521 | !$permissionManager->userHasAllRights( |
1522 | $user, ...(array)( $namespaceProtection[ NS_TRANSLATIONS ] ?? [] ) |
1523 | ) |
1524 | ) { |
1525 | return; |
1526 | } |
1527 | |
1528 | $tab = [ |
1529 | 'text' => $skin->msg( 'tpt-tab-translate' )->text(), |
1530 | 'href' => $page->getTranslationUrl( $code ), |
1531 | ]; |
1532 | |
1533 | // Get the position of the viewsource tab within the array (if any) |
1534 | $viewsourcePos = array_keys( array_keys( $tabs['views'] ), 'viewsource', true )[0] ?? null; |
1535 | |
1536 | if ( $viewsourcePos !== null ) { |
1537 | // Remove the viewsource tab and insert the translate tab at its place. Showing the tooltip |
1538 | // of the viewsource tab for the translate tab would be confusing. |
1539 | array_splice( $tabs['views'], $viewsourcePos, 1, [ 'translate' => $tab ] ); |
1540 | } else { |
1541 | // We have neither an edit tab nor a viewsource tab to replace with the translate tab, |
1542 | // put the translate tab at the end |
1543 | $tabs['views']['translate'] = $tab; |
1544 | } |
1545 | } |
1546 | } |
1547 | |
1548 | /** |
1549 | * Hook to update source and destination translation pages on moving translation units |
1550 | * Hook: PageMoveComplete |
1551 | * |
1552 | * @param LinkTarget $oldLinkTarget |
1553 | * @param LinkTarget $newLinkTarget |
1554 | * @param UserIdentity $userIdentity |
1555 | * @param int $oldid |
1556 | * @param int $newid |
1557 | * @param string $reason |
1558 | * @param RevisionRecord $revisionRecord |
1559 | */ |
1560 | public static function onMovePageTranslationUnits( |
1561 | LinkTarget $oldLinkTarget, |
1562 | LinkTarget $newLinkTarget, |
1563 | UserIdentity $userIdentity, |
1564 | int $oldid, |
1565 | int $newid, |
1566 | string $reason, |
1567 | RevisionRecord $revisionRecord |
1568 | ) { |
1569 | $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity ); |
1570 | // MoveTranslatableBundleJob takes care of handling updates because it performs |
1571 | // a lot of moves at once. As a performance optimization, skip this hook if |
1572 | // we detect moves from that job. As there isn't a good way to pass information |
1573 | // to this hook what originated the move, we use some heuristics. |
1574 | if ( defined( 'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) { |
1575 | return; |
1576 | } |
1577 | |
1578 | $oldTitle = Title::newFromLinkTarget( $oldLinkTarget ); |
1579 | $newTitle = Title::newFromLinkTarget( $newLinkTarget ); |
1580 | $groupLast = null; |
1581 | foreach ( [ $oldTitle, $newTitle ] as $title ) { |
1582 | $handle = new MessageHandle( $title ); |
1583 | // Documentation pages are never translation pages |
1584 | if ( !$handle->isValid() || $handle->isDoc() ) { |
1585 | continue; |
1586 | } |
1587 | |
1588 | $group = $handle->getGroup(); |
1589 | if ( !$group instanceof WikiPageMessageGroup ) { |
1590 | continue; |
1591 | } |
1592 | |
1593 | $language = $handle->getCode(); |
1594 | |
1595 | // Ignore pages such as Translations:Page/unit without language code |
1596 | if ( $language === '' ) { |
1597 | continue; |
1598 | } |
1599 | |
1600 | // Update the page only once if source and destination units |
1601 | // belong to the same page |
1602 | if ( $group !== $groupLast ) { |
1603 | $groupLast = $group; |
1604 | $page = TranslatablePage::newFromTitle( $group->getTitle() ); |
1605 | self::updateTranslationPage( $page, $language, $user, 0, $reason ); |
1606 | } |
1607 | } |
1608 | } |
1609 | |
1610 | /** |
1611 | * Hook to update translation page on deleting a translation unit |
1612 | * Hook: ArticleDeleteComplete |
1613 | * @param WikiPage $unit |
1614 | * @param User $user |
1615 | * @param string $reason |
1616 | * @param int $id |
1617 | * @param Content $content |
1618 | * @param ManualLogEntry $logEntry |
1619 | */ |
1620 | public static function onDeleteTranslationUnit( |
1621 | WikiPage $unit, |
1622 | User $user, |
1623 | $reason, |
1624 | $id, |
1625 | $content, |
1626 | $logEntry |
1627 | ) { |
1628 | $title = $unit->getTitle(); |
1629 | $handle = new MessageHandle( $title ); |
1630 | if ( !$handle->isValid() ) { |
1631 | return; |
1632 | } |
1633 | |
1634 | $group = $handle->getGroup(); |
1635 | if ( !$group instanceof WikiPageMessageGroup ) { |
1636 | return; |
1637 | } |
1638 | |
1639 | $mwServices = MediaWikiServices::getInstance(); |
1640 | // During deletions this may cause creation of a lot of duplicate jobs. It is expected that |
1641 | // job queue will deduplicate them to reduce the number of jobs actually run. |
1642 | $mwServices->getJobQueueGroup()->push( |
1643 | RebuildMessageGroupStatsJob::newRefreshGroupsJob( [ $group->getId() ] ) |
1644 | ); |
1645 | |
1646 | // Logic to update translation pages, skipped if we are in a middle of a deletion |
1647 | if ( self::$isDeleteTranslatableBundleJobRunning ) { |
1648 | return; |
1649 | } |
1650 | |
1651 | $target = $group->getTitle(); |
1652 | $langCode = $handle->getCode(); |
1653 | $fname = __METHOD__; |
1654 | |
1655 | $dbw = $mwServices->getDBLoadBalancer()->getConnection( DB_PRIMARY ); |
1656 | $callback = function () use ( |
1657 | $dbw, |
1658 | $target, |
1659 | $handle, |
1660 | $langCode, |
1661 | $user, |
1662 | $reason, |
1663 | $fname |
1664 | ) { |
1665 | $translationPageTitle = $target->getSubpage( $langCode ); |
1666 | // Do a more thorough check for the translation page in case the translation page is deleted in a |
1667 | // different transaction. |
1668 | if ( !$translationPageTitle || !$translationPageTitle->exists( IDBAccessObject::READ_LATEST ) ) { |
1669 | return; |
1670 | } |
1671 | |
1672 | $dbw->startAtomic( $fname ); |
1673 | |
1674 | $page = TranslatablePage::newFromTitle( $target ); |
1675 | |
1676 | if ( !$handle->isDoc() ) { |
1677 | $unitTitle = $handle->getTitle(); |
1678 | // Assume that $user and $reason for the first deletion is the same for all |
1679 | self::updateTranslationPage( |
1680 | $page, $langCode, $user, 0, $reason, RenderTranslationPageJob::ACTION_DELETE, $unitTitle |
1681 | ); |
1682 | } |
1683 | |
1684 | $dbw->endAtomic( $fname ); |
1685 | }; |
1686 | |
1687 | $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ ); |
1688 | } |
1689 | |
1690 | /** |
1691 | * Removes translation pages from the list of page titles to be edited |
1692 | * Hook: ReplaceTextFilterPageTitlesForEdit |
1693 | */ |
1694 | public static function onReplaceTextFilterPageTitlesForEdit( array &$titles ): void { |
1695 | foreach ( $titles as $index => $title ) { |
1696 | $handle = new MessageHandle( $title ); |
1697 | if ( Utilities::isTranslationPage( $handle ) ) { |
1698 | unset( $titles[ $index ] ); |
1699 | } |
1700 | } |
1701 | } |
1702 | |
1703 | /** |
1704 | * Removes translatable and translation pages from the list of titles to be renamed |
1705 | * Hook: ReplaceTextFilterPageTitlesForRename |
1706 | */ |
1707 | public static function onReplaceTextFilterPageTitlesForRename( array &$titles ): void { |
1708 | foreach ( $titles as $index => $title ) { |
1709 | $handle = new MessageHandle( $title ); |
1710 | if ( |
1711 | TranslatablePage::isSourcePage( $title ) || |
1712 | Utilities::isTranslationPage( $handle ) |
1713 | ) { |
1714 | unset( $titles[ $index ] ); |
1715 | } |
1716 | } |
1717 | } |
1718 | } |