Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
59.83% |
140 / 234 |
|
44.44% |
16 / 36 |
CRAP | |
0.00% |
0 / 1 |
TranslatablePage | |
60.09% |
140 / 233 |
|
44.44% |
16 / 36 |
362.03 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
newFromText | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
newFromRevision | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
2.01 | |||
newFromTitle | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getTitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPageIdentity | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getText | |
76.19% |
16 / 21 |
|
0.00% |
0 / 1 |
7.66 | |||
getRevision | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSourceLanguageCode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMessageGroupId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMessageGroupIdFromTitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMessageGroup | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
4.12 | |||
hasPageDisplayTitle | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getPageDisplayTitle | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getStrippedSourcePageText | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getTranslationPageFromTitle | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getTranslationPage | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
1 | |||
addMarkedTag | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
addReadyTag | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
getMarkedTag | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getReadyTag | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTranslationUrl | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getMarkedRevs | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getTranslationPages | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
5.01 | |||
getTranslationUnitPages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTranslationPercentages | |
15.79% |
3 / 19 |
|
0.00% |
0 / 1 |
19.93 | |||
getTransRev | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
supportsTransclusion | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getRevisionRecordWithFallback | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
isMoveable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isDeletable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isTranslationPage | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
3.01 | |||
parseTranslationUnit | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
isSourcePage | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
2 | |||
clearSourcePageCache | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
determineStatus | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\PageTranslation; |
5 | |
6 | use IDBAccessObject; |
7 | use LogicException; |
8 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups; |
9 | use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore; |
10 | use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundle; |
11 | use MediaWiki\Extension\Translate\MessageLoading\MessageHandle; |
12 | use MediaWiki\Extension\Translate\Services; |
13 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
14 | use MediaWiki\Languages\LanguageNameUtils; |
15 | use MediaWiki\Linker\LinkTarget; |
16 | use MediaWiki\MediaWikiServices; |
17 | use MediaWiki\Page\PageIdentity; |
18 | use MediaWiki\Page\PageReference; |
19 | use MediaWiki\Revision\RevisionRecord; |
20 | use MediaWiki\Revision\SlotRecord; |
21 | use MediaWiki\Title\Title; |
22 | use MessageGroupStats; |
23 | use RuntimeException; |
24 | use SpecialPage; |
25 | use TextContent; |
26 | use Wikimedia\Rdbms\Database; |
27 | use Wikimedia\Rdbms\IResultWrapper; |
28 | use Wikimedia\Rdbms\SelectQueryBuilder; |
29 | use WikiPageMessageGroup; |
30 | |
31 | /** |
32 | * Mixed bag of methods related to translatable pages. |
33 | * @author Niklas Laxström |
34 | * @license GPL-2.0-or-later |
35 | * @ingroup PageTranslation |
36 | */ |
37 | class TranslatablePage extends TranslatableBundle { |
38 | /** |
39 | * List of keys in the metadata table that need to be handled for moves and deletions |
40 | * @phpcs-require-sorted-array |
41 | */ |
42 | public const METADATA_KEYS = [ |
43 | 'maxid', |
44 | 'priorityforce', |
45 | 'prioritylangs', |
46 | 'priorityreason', |
47 | 'transclusion', |
48 | 'version' |
49 | ]; |
50 | /** @var string Name of the section which contains the translated page title. */ |
51 | public const DISPLAY_TITLE_UNIT_ID = 'Page display title'; |
52 | |
53 | protected PageIdentity $title; |
54 | /** @var RevTagStore */ |
55 | protected $revTagStore; |
56 | /** @var ?string Text contents of the page. */ |
57 | protected $text; |
58 | /** @var ?int Revision of the page, if applicable. */ |
59 | protected $revision; |
60 | /** @var string From which source this object was constructed: text, revision or title */ |
61 | protected $source; |
62 | /** @var ?bool Whether the title should be translated */ |
63 | protected $pageDisplayTitle; |
64 | /** @var ?string */ |
65 | private $targetLanguage; |
66 | |
67 | protected function __construct( PageIdentity $title ) { |
68 | $this->title = $title; |
69 | $this->revTagStore = Services::getInstance()->getRevTagStore(); |
70 | } |
71 | |
72 | /** |
73 | * Constructs a translatable page from given text. |
74 | * Some functions will fail unless you set revision |
75 | * parameter manually. |
76 | */ |
77 | public static function newFromText( Title $title, string $text ): self { |
78 | $obj = new self( $title ); |
79 | $obj->text = $text; |
80 | $obj->source = 'text'; |
81 | |
82 | return $obj; |
83 | } |
84 | |
85 | /** |
86 | * Constructs a translatable page from given revision. |
87 | * The revision must belong to the title given or unspecified |
88 | * behavior will happen. |
89 | */ |
90 | public static function newFromRevision( PageIdentity $title, int $revision ): self { |
91 | $rev = MediaWikiServices::getInstance() |
92 | ->getRevisionLookup() |
93 | ->getRevisionByTitle( $title, $revision ); |
94 | if ( $rev === null ) { |
95 | throw new RuntimeException( 'Revision is null' ); |
96 | } |
97 | |
98 | $obj = new self( $title ); |
99 | $obj->source = 'revision'; |
100 | $obj->revision = $revision; |
101 | |
102 | return $obj; |
103 | } |
104 | |
105 | /** |
106 | * Constructs a translatable page from title. |
107 | * The text of last marked revision is loaded when needed. |
108 | */ |
109 | public static function newFromTitle( PageIdentity $title ): self { |
110 | $obj = new self( $title ); |
111 | $obj->source = 'title'; |
112 | |
113 | return $obj; |
114 | } |
115 | |
116 | /** @inheritDoc */ |
117 | public function getTitle(): Title { |
118 | return Title::castFromPageIdentity( $this->title ); |
119 | } |
120 | |
121 | public function getPageIdentity(): PageIdentity { |
122 | return $this->title; |
123 | } |
124 | |
125 | /** Returns the text for this translatable page. */ |
126 | public function getText(): string { |
127 | if ( $this->text !== null ) { |
128 | return $this->text; |
129 | } |
130 | |
131 | if ( $this->source === 'title' ) { |
132 | $revision = $this->getMarkedTag(); |
133 | if ( !is_int( $revision ) ) { |
134 | throw new LogicException( |
135 | "Trying to load a text for {$this->getPageIdentity()} which is not marked for translation" |
136 | ); |
137 | } |
138 | $this->revision = $revision; |
139 | } |
140 | |
141 | $flags = Utilities::shouldReadFromPrimary() |
142 | ? IDBAccessObject::READ_LATEST |
143 | : IDBAccessObject::READ_NORMAL; |
144 | $rev = MediaWikiServices::getInstance() |
145 | ->getRevisionLookup() |
146 | ->getRevisionByTitle( $this->getPageIdentity(), $this->revision, $flags ); |
147 | $content = $rev->getContent( SlotRecord::MAIN ); |
148 | $text = ( $content instanceof TextContent ) ? $content->getText() : null; |
149 | |
150 | if ( !is_string( $text ) ) { |
151 | throw new RuntimeException( "Failed to load text for {$this->getPageIdentity()}" ); |
152 | } |
153 | |
154 | $this->text = $text; |
155 | |
156 | return $this->text; |
157 | } |
158 | |
159 | /** |
160 | * Revision is null if object was constructed using newFromText. |
161 | * @return null|int |
162 | */ |
163 | public function getRevision(): ?int { |
164 | return $this->revision; |
165 | } |
166 | |
167 | /** |
168 | * Returns the source language of this translatable page. In other words |
169 | * the language in which the page without language code is written. |
170 | * @since 2013-01-28 |
171 | */ |
172 | public function getSourceLanguageCode(): string { |
173 | return $this->getTitle()->getPageLanguage()->getCode(); |
174 | } |
175 | |
176 | /** @inheritDoc */ |
177 | public function getMessageGroupId(): string { |
178 | return self::getMessageGroupIdFromTitle( $this->getPageIdentity() ); |
179 | } |
180 | |
181 | /** Constructs MessageGroup id for any title. */ |
182 | public static function getMessageGroupIdFromTitle( PageReference $page ): string { |
183 | return 'page-' . MediaWikiServices::getInstance()->getTitleFormatter()->getPrefixedText( $page ); |
184 | } |
185 | |
186 | /** |
187 | * Returns MessageGroup used for translating this page. It may still be empty |
188 | * if the page has not been ever marked. |
189 | */ |
190 | public function getMessageGroup(): ?WikiPageMessageGroup { |
191 | $groupId = $this->getMessageGroupId(); |
192 | $group = MessageGroups::getGroup( $groupId ); |
193 | if ( !$group || $group instanceof WikiPageMessageGroup ) { |
194 | return $group; |
195 | } |
196 | |
197 | throw new RuntimeException( |
198 | "Expected $groupId to be of type WikiPageMessageGroup; got " . |
199 | get_class( $group ) |
200 | ); |
201 | } |
202 | |
203 | /** Check whether title is marked for translation */ |
204 | public function hasPageDisplayTitle(): bool { |
205 | // Cached value |
206 | if ( $this->pageDisplayTitle !== null ) { |
207 | return $this->pageDisplayTitle; |
208 | } |
209 | |
210 | // Check if title section exists in list of sections |
211 | $factory = Services::getInstance()->getTranslationUnitStoreFactory(); |
212 | $store = $factory->getReader( $this->getPageIdentity() ); |
213 | $this->pageDisplayTitle = in_array( self::DISPLAY_TITLE_UNIT_ID, $store->getNames() ); |
214 | |
215 | return $this->pageDisplayTitle; |
216 | } |
217 | |
218 | /** Get translated page title. */ |
219 | public function getPageDisplayTitle( string $languageCode ): ?string { |
220 | // Return null if title not marked for translation |
221 | if ( !$this->hasPageDisplayTitle() ) { |
222 | return null; |
223 | } |
224 | |
225 | // Display title from DB |
226 | $section = str_replace( ' ', '_', self::DISPLAY_TITLE_UNIT_ID ); |
227 | $page = MediaWikiServices::getInstance()->getTitleFormatter()->getPrefixedDBkey( $this->getPageIdentity() ); |
228 | |
229 | try { |
230 | $group = $this->getMessageGroup(); |
231 | } catch ( RuntimeException $e ) { |
232 | return null; |
233 | } |
234 | |
235 | // Sanity check, seems to happen during moves |
236 | if ( !$group ) { |
237 | return null; |
238 | } |
239 | |
240 | return $group->getMessage( "$page/$section", $languageCode, IDBAccessObject::READ_NORMAL ); |
241 | } |
242 | |
243 | public function getStrippedSourcePageText(): string { |
244 | $parser = Services::getInstance()->getTranslatablePageParser(); |
245 | $text = $parser->cleanupTags( $this->getText() ); |
246 | $text = preg_replace( '~<languages\s*/>\n?~s', '', $text ); |
247 | |
248 | return $text; |
249 | } |
250 | |
251 | public static function getTranslationPageFromTitle( Title $title ): ?TranslationPage { |
252 | $self = self::isTranslationPage( $title ); |
253 | return $self ? $self->getTranslationPage( $self->targetLanguage ) : null; |
254 | } |
255 | |
256 | public function getTranslationPage( string $targetLanguage ): TranslationPage { |
257 | $mwServices = MediaWikiServices::getInstance(); |
258 | $config = $mwServices->getMainConfig(); |
259 | $services = Services::getInstance(); |
260 | $parser = $services->getTranslatablePageParser(); |
261 | $parserOutput = $parser->parse( $this->getText() ); |
262 | $pageVersion = (int)$services->getMessageGroupMetadata() |
263 | ->get( $this->getMessageGroupId(), 'version' ); |
264 | $wrapUntranslated = $pageVersion >= 2; |
265 | $languageFactory = $mwServices->getLanguageFactory(); |
266 | |
267 | return new TranslationPage( |
268 | $parserOutput, |
269 | $this->getMessageGroup(), |
270 | $languageFactory->getLanguage( $targetLanguage ), |
271 | $languageFactory->getLanguage( $this->getSourceLanguageCode() ), |
272 | $config->get( 'TranslateKeepOutdatedTranslations' ), |
273 | $wrapUntranslated, |
274 | $this->getTitle() |
275 | ); |
276 | } |
277 | |
278 | /** Adds a tag which indicates that this page is suitable for translation. */ |
279 | public function addMarkedTag( int $revision, array $value = null ) { |
280 | $this->revTagStore->replaceTag( $this->getPageIdentity(), RevTagStore::TP_MARK_TAG, $revision, $value ); |
281 | self::clearSourcePageCache(); |
282 | } |
283 | |
284 | /** Adds a tag which indicates that this page source is ready for marking for translation. */ |
285 | public function addReadyTag( int $revision ): void { |
286 | $this->revTagStore->replaceTag( $this->getPageIdentity(), RevTagStore::TP_READY_TAG, $revision ); |
287 | if ( !self::isSourcePage( $this->getPageIdentity() ) ) { |
288 | self::clearSourcePageCache(); |
289 | } |
290 | } |
291 | |
292 | /** Returns the latest revision which has marked tag, if any. */ |
293 | public function getMarkedTag(): ?int { |
294 | return $this->revTagStore->getLatestRevisionWithTag( $this->getPageIdentity(), RevTagStore::TP_MARK_TAG ); |
295 | } |
296 | |
297 | /** Returns the latest revision which has ready tag, if any. */ |
298 | public function getReadyTag(): ?int { |
299 | return $this->revTagStore->getLatestRevisionWithTag( $this->getPageIdentity(), RevTagStore::TP_READY_TAG ); |
300 | } |
301 | |
302 | /** |
303 | * Produces a link to translation view of a translation page. |
304 | * @param string|bool $code MediaWiki language code. Default: false. |
305 | * @return string Relative url |
306 | */ |
307 | public function getTranslationUrl( $code = false ): string { |
308 | $params = [ |
309 | 'group' => $this->getMessageGroupId(), |
310 | 'action' => 'page', |
311 | 'filter' => '', |
312 | 'language' => $code, |
313 | ]; |
314 | |
315 | $translate = SpecialPage::getTitleFor( 'Translate' ); |
316 | |
317 | return $translate->getLocalURL( $params ); |
318 | } |
319 | |
320 | public function getMarkedRevs(): IResultWrapper { |
321 | $db = Utilities::getSafeReadDB(); |
322 | |
323 | return $db->newSelectQueryBuilder() |
324 | ->select( [ 'rt_revision', 'rt_value' ] ) |
325 | ->from( 'revtag' ) |
326 | ->where( [ |
327 | 'rt_page' => $this->getPageIdentity()->getId(), |
328 | 'rt_type' => RevTagStore::TP_MARK_TAG, |
329 | ] ) |
330 | ->orderBy( 'rt_revision', SelectQueryBuilder::SORT_DESC ) |
331 | ->caller( __METHOD__ ) |
332 | ->fetchResultSet(); |
333 | } |
334 | |
335 | /** @inheritDoc */ |
336 | public function getTranslationPages(): array { |
337 | $mwServices = MediaWikiServices::getInstance(); |
338 | |
339 | $messageGroup = $this->getMessageGroup(); |
340 | $knownLanguageCodes = $messageGroup ? $messageGroup->getTranslatableLanguages() : null; |
341 | $knownLanguageCodes ??= Utilities::getLanguageNames( LanguageNameUtils::AUTONYMS ); |
342 | |
343 | $prefixedDbTitleKey = $this->getPageIdentity()->getDBkey() . '/'; |
344 | $baseNamespace = $this->getPageIdentity()->getNamespace(); |
345 | |
346 | // Build a link batch query for all translation pages |
347 | $linkBatch = $mwServices->getLinkBatchFactory()->newLinkBatch(); |
348 | foreach ( array_keys( $knownLanguageCodes ) as $code ) { |
349 | $linkBatch->add( $baseNamespace, $prefixedDbTitleKey . $code ); |
350 | } |
351 | |
352 | $translationPages = []; |
353 | foreach ( $linkBatch->getPageIdentities() as $pageIdentity ) { |
354 | if ( $pageIdentity->exists() ) { |
355 | $translationPages[] = Title::castFromPageIdentity( $pageIdentity ); |
356 | } |
357 | } |
358 | |
359 | return $translationPages; |
360 | } |
361 | |
362 | /** @inheritDoc */ |
363 | public function getTranslationUnitPages( ?string $code = null ): array { |
364 | return $this->getTranslationUnitPagesByTitle( $this->title, $code ); |
365 | } |
366 | |
367 | public function getTranslationPercentages(): array { |
368 | // Calculate percentages for the available translations |
369 | try { |
370 | $group = $this->getMessageGroup(); |
371 | } catch ( RuntimeException $e ) { |
372 | return []; |
373 | } |
374 | |
375 | if ( !$group ) { |
376 | return []; |
377 | } |
378 | |
379 | $titles = $this->getTranslationPages(); |
380 | $temp = MessageGroupStats::forGroup( $this->getMessageGroupId() ); |
381 | $stats = []; |
382 | |
383 | foreach ( $titles as $t ) { |
384 | $handle = new MessageHandle( $t ); |
385 | $code = $handle->getCode(); |
386 | |
387 | // Sometimes we want to display 0.00 for pages for which translation |
388 | // hasn't started yet. |
389 | $stats[$code] = 0.00; |
390 | if ( ( $temp[$code][MessageGroupStats::TOTAL] ?? 0 ) > 0 ) { |
391 | $total = $temp[$code][MessageGroupStats::TOTAL]; |
392 | $translated = $temp[$code][MessageGroupStats::TRANSLATED]; |
393 | $percentage = $translated / $total; |
394 | $stats[$code] = sprintf( '%.2f', $percentage ); |
395 | } |
396 | } |
397 | |
398 | // Content language is always up-to-date |
399 | $stats[$this->getSourceLanguageCode()] = 1.00; |
400 | |
401 | return $stats; |
402 | } |
403 | |
404 | public function getTransRev( string $suffix ) { |
405 | $title = Title::makeTitle( NS_TRANSLATIONS, $suffix ); |
406 | |
407 | $db = Utilities::getSafeReadDB(); |
408 | |
409 | return $db->newSelectQueryBuilder() |
410 | ->select( 'rt_value' ) |
411 | ->from( 'revtag' ) |
412 | ->where( [ |
413 | 'rt_page' => $title->getArticleID(), |
414 | 'rt_type' => RevTagStore::TRANSVER_PROP, |
415 | ] ) |
416 | ->orderBy( 'rt_revision', SelectQueryBuilder::SORT_DESC ) |
417 | ->caller( __METHOD__ ) |
418 | ->fetchField(); |
419 | } |
420 | |
421 | public function supportsTransclusion(): ?bool { |
422 | $transclusion = Services::getInstance() |
423 | ->getMessageGroupMetadata() |
424 | ->get( $this->getMessageGroupId(), 'transclusion' ); |
425 | if ( $transclusion === false ) { |
426 | return null; |
427 | } |
428 | |
429 | return $transclusion === '1'; |
430 | } |
431 | |
432 | public function getRevisionRecordWithFallback(): ?RevisionRecord { |
433 | $title = $this->getTitle(); |
434 | $store = MediaWikiServices::getInstance()->getRevisionStore(); |
435 | $revRecord = $store->getRevisionByTitle( $title->getSubpage( $this->targetLanguage ) ); |
436 | if ( $revRecord ) { |
437 | return $revRecord; |
438 | } |
439 | |
440 | // Fetch the source fallback |
441 | return $store->getRevisionByTitle( $title->getSubpage( $this->getSourceLanguageCode() ) ); |
442 | } |
443 | |
444 | /** @inheritDoc */ |
445 | public function isMoveable(): bool { |
446 | return $this->getMarkedTag() !== null; |
447 | } |
448 | |
449 | /** @inheritDoc */ |
450 | public function isDeletable(): bool { |
451 | return $this->getMarkedTag() !== null; |
452 | } |
453 | |
454 | /** @return bool|self */ |
455 | public static function isTranslationPage( Title $title ) { |
456 | $handle = new MessageHandle( $title ); |
457 | if ( !Utilities::isTranslationPage( $handle ) ) { |
458 | return false; |
459 | } |
460 | |
461 | $languageCode = $handle->getCode(); |
462 | $newTitle = $handle->getTitleForBase(); |
463 | |
464 | $page = self::newFromTitle( $newTitle ); |
465 | |
466 | if ( $page->getMarkedTag() === null ) { |
467 | return false; |
468 | } |
469 | |
470 | $page->targetLanguage = $languageCode; |
471 | |
472 | return $page; |
473 | } |
474 | |
475 | /** Helper to guess translation page from translation unit. */ |
476 | public static function parseTranslationUnit( LinkTarget $translationUnit ): array { |
477 | // Format is Translations:SourcePageNamespace:SourcePageName/SectionName/LanguageCode. |
478 | // We will drop the namespace immediately here. |
479 | $parts = explode( '/', $translationUnit->getText() ); |
480 | |
481 | // LanguageCode and SectionName are guaranteed to not have '/'. |
482 | $language = array_pop( $parts ); |
483 | $section = array_pop( $parts ); |
484 | $sourcepage = implode( '/', $parts ); |
485 | |
486 | return [ |
487 | 'sourcepage' => $sourcepage, |
488 | 'section' => $section, |
489 | 'language' => $language |
490 | ]; |
491 | } |
492 | |
493 | public static function isSourcePage( PageIdentity $page ): bool { |
494 | if ( !$page->exists() ) { |
495 | // No point in loading all translatable pages if the page |
496 | // doesn’t exist. This also avoids PreconditionExceptions |
497 | // if $page is a Title pointing to a non-proper page like |
498 | // a special page. |
499 | return false; |
500 | } |
501 | |
502 | $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); |
503 | $cacheKey = $cache->makeKey( 'pagetranslation', 'sourcepages' ); |
504 | |
505 | $translatablePageIds = $cache->getWithSetCallback( |
506 | $cacheKey, |
507 | $cache::TTL_HOUR * 2, |
508 | static function ( $oldValue, &$ttl, array &$setOpts ) { |
509 | $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA ); |
510 | $setOpts += Database::getCacheSetOptions( $dbr ); |
511 | |
512 | return RevTagStore::getTranslatableBundleIds( |
513 | RevTagStore::TP_MARK_TAG, RevTagStore::TP_READY_TAG |
514 | ); |
515 | }, |
516 | [ |
517 | 'checkKeys' => [ $cacheKey ], |
518 | 'pcTTL' => $cache::TTL_PROC_SHORT, |
519 | 'pcGroup' => __CLASS__ . ':1', |
520 | 'version' => 2, |
521 | ] |
522 | ); |
523 | |
524 | return isset( $translatablePageIds[$page->getId()] ); |
525 | } |
526 | |
527 | /** Clears the source page cache */ |
528 | public static function clearSourcePageCache(): void { |
529 | $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); |
530 | $cache->touchCheckKey( $cache->makeKey( 'pagetranslation', 'sourcepages' ) ); |
531 | } |
532 | |
533 | public static function determineStatus( |
534 | ?int $readyRevisionId, |
535 | ?int $markRevisionId, |
536 | int $latestRevisionId |
537 | ): ?TranslatablePageStatus { |
538 | $status = null; |
539 | if ( $markRevisionId === null ) { |
540 | // Never marked, check that the latest version is ready |
541 | if ( $readyRevisionId === $latestRevisionId ) { |
542 | $status = TranslatablePageStatus::PROPOSED; |
543 | } else { |
544 | // Otherwise, ignore such pages |
545 | return null; |
546 | } |
547 | } elseif ( $readyRevisionId === $latestRevisionId ) { |
548 | if ( $markRevisionId === $readyRevisionId ) { |
549 | // Marked and latest version is fine |
550 | $status = TranslatablePageStatus::ACTIVE; |
551 | } else { |
552 | $status = TranslatablePageStatus::OUTDATED; |
553 | } |
554 | } else { |
555 | // Marked but latest version is not fine |
556 | $status = TranslatablePageStatus::BROKEN; |
557 | } |
558 | |
559 | return new TranslatablePageStatus( $status ); |
560 | } |
561 | } |
562 | |
563 | class_alias( TranslatablePage::class, 'TranslatablePage' ); |