Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 127 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
FuzzyTranslationsMaintenanceScript | |
0.00% |
0 / 127 |
|
0.00% |
0 / 8 |
420 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
2 | |||
initServices | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
fuzzyTranslations | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getMessageContentsFromRows | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
getPagesForPattern | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
12 | |||
getPagesForUser | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
6 | |||
updateMessage | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\Diagnostics; |
5 | |
6 | use CommentStoreComment; |
7 | use ContentHandler; |
8 | use IDBAccessObject; |
9 | use MediaWiki\Extension\Translate\SystemUsers\FuzzyBot; |
10 | use MediaWiki\Extension\Translate\Utilities\BaseMaintenanceScript; |
11 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
12 | use MediaWiki\MediaWikiServices; |
13 | use MediaWiki\Page\WikiPageFactory; |
14 | use MediaWiki\Revision\RevisionStore; |
15 | use MediaWiki\Revision\SlotRecord; |
16 | use MediaWiki\Title\Title; |
17 | use MediaWiki\User\ActorMigration; |
18 | use MediaWiki\User\UserFactory; |
19 | use User; |
20 | use Wikimedia\Rdbms\ILoadBalancer; |
21 | use Wikimedia\Rdbms\IResultWrapper; |
22 | |
23 | /** |
24 | * @since 2022.01 |
25 | * @license GPL-2.0-or-later |
26 | * @author Niklas Laxström |
27 | */ |
28 | class FuzzyTranslationsMaintenanceScript extends BaseMaintenanceScript { |
29 | private ActorMigration $actorMigration; |
30 | private UserFactory $userFactory; |
31 | private RevisionStore $revisionStore; |
32 | private ILoadBalancer $DBLoadBalancer; |
33 | private WikiPageFactory $wikiPageFactory; |
34 | |
35 | public function __construct() { |
36 | parent::__construct(); |
37 | $this->addDescription( 'Fuzzy bot command line script.' ); |
38 | $this->addArg( |
39 | 'arg', |
40 | 'Title pattern or username if user option is provided.' |
41 | ); |
42 | $this->addOption( |
43 | 'really', |
44 | '(optional) Really fuzzy, no dry-run' |
45 | ); |
46 | $this->addOption( |
47 | 'skiplanguages', |
48 | '(optional) Skip some languages (comma separated)', |
49 | self::OPTIONAL, |
50 | self::HAS_ARG |
51 | ); |
52 | $this->addOption( |
53 | 'comment', |
54 | '(optional) Comment for updating', |
55 | self::OPTIONAL, |
56 | self::HAS_ARG |
57 | ); |
58 | $this->addOption( |
59 | 'user', |
60 | '(optional) Fuzzy the translations made by user given as an argument.', |
61 | self::OPTIONAL, |
62 | self::NO_ARG |
63 | ); |
64 | $this->requireExtension( 'Translate' ); |
65 | } |
66 | |
67 | private function initServices(): void { |
68 | $mwServices = MediaWikiServices::getInstance(); |
69 | $this->actorMigration = $mwServices->getActorMigration(); |
70 | $this->userFactory = $mwServices->getUserFactory(); |
71 | $this->revisionStore = $mwServices->getRevisionStore(); |
72 | $this->DBLoadBalancer = $mwServices->getDBLoadBalancer(); |
73 | $this->wikiPageFactory = $mwServices->getWikiPageFactory(); |
74 | } |
75 | |
76 | public function execute(): void { |
77 | $this->initServices(); |
78 | |
79 | $skipLanguages = []; |
80 | if ( $this->hasOption( 'skiplanguages' ) ) { |
81 | $skipLanguages = array_map( |
82 | 'trim', |
83 | explode( ',', $this->getOption( 'skiplanguages' ) ) |
84 | ); |
85 | } |
86 | |
87 | if ( $this->hasOption( 'user' ) ) { |
88 | $user = $this->userFactory->newFromName( $this->getArg( 0 ) ); |
89 | $pages = $this->getPagesForUser( $user, $skipLanguages ); |
90 | } else { |
91 | $pages = $this->getPagesForPattern( $this->getArg( 0 ), $skipLanguages ); |
92 | } |
93 | |
94 | $dryrun = !$this->hasOption( 'really' ); |
95 | $comment = $this->getOption( 'comment' ); |
96 | $this->fuzzyTranslations( $pages, $dryrun, $comment ); |
97 | } |
98 | |
99 | private function fuzzyTranslations( array $pages, bool $dryrun, ?string $comment ): void { |
100 | $count = count( $pages ); |
101 | $this->output( "Found $count pages to update.", 'pagecount' ); |
102 | |
103 | foreach ( $pages as [ $title, $text ] ) { |
104 | $this->updateMessage( $title, TRANSLATE_FUZZY . $text, $dryrun, $comment ); |
105 | } |
106 | } |
107 | |
108 | /** |
109 | * Gets the message contents from database rows. |
110 | * @param IResultWrapper $rows |
111 | * @return array containing page titles and the text content of the page |
112 | */ |
113 | private function getMessageContentsFromRows( IResultWrapper $rows ): array { |
114 | $messagesContents = []; |
115 | $slots = $this->revisionStore->getContentBlobsForBatch( $rows, [ SlotRecord::MAIN ] )->getValue(); |
116 | foreach ( $rows as $row ) { |
117 | $title = Title::makeTitle( $row->page_namespace, $row->page_title ); |
118 | if ( isset( $slots[$row->rev_id] ) ) { |
119 | $text = $slots[$row->rev_id][SlotRecord::MAIN]->blob_data; |
120 | } else { |
121 | $content = $this->revisionStore |
122 | ->newRevisionFromRow( $row, IDBAccessObject::READ_NORMAL, $title ) |
123 | ->getContent( SlotRecord::MAIN ); |
124 | $text = Utilities::getTextFromTextContent( $content ); |
125 | } |
126 | $messagesContents[] = [ $title, $text ]; |
127 | } |
128 | return $messagesContents; |
129 | } |
130 | |
131 | /** Searches pages that match given patterns */ |
132 | private function getPagesForPattern( string $pattern, array $skipLanguages = [] ): array { |
133 | $dbr = $this->DBLoadBalancer->getMaintenanceConnectionRef( DB_REPLICA ); |
134 | |
135 | $conds = [ |
136 | 'page_latest=rev_id', |
137 | ]; |
138 | |
139 | $title = Title::newFromText( $pattern ); |
140 | if ( $title->inNamespace( NS_MAIN ) ) { |
141 | $namespace = $this->getConfig()->get( 'TranslateMessageNamespaces' ); |
142 | } else { |
143 | $namespace = $title->getNamespace(); |
144 | } |
145 | |
146 | $conds['page_namespace'] = $namespace; |
147 | $conds[] = 'page_title' . $dbr->buildLike( $title->getDBkey(), $dbr->anyString() ); |
148 | |
149 | if ( count( $skipLanguages ) ) { |
150 | $skiplist = $dbr->makeList( $skipLanguages ); |
151 | $conds[] = "substring_index(page_title, '/', -1) NOT IN ($skiplist)"; |
152 | } |
153 | |
154 | $queryInfo = $this->revisionStore->getQueryInfo( [ 'page' ] ); |
155 | $rows = $dbr->newSelectQueryBuilder() |
156 | ->select( $queryInfo['fields'] ) |
157 | ->tables( $queryInfo['tables'] ) |
158 | ->joinConds( $queryInfo['joins'] ) |
159 | ->where( $conds ) |
160 | ->caller( __METHOD__ ) |
161 | ->fetchResultSet(); |
162 | return $this->getMessageContentsFromRows( $rows ); |
163 | } |
164 | |
165 | private function getPagesForUser( User $user, array $skipLanguages = [] ): array { |
166 | $dbr = $this->DBLoadBalancer->getMaintenanceConnectionRef( DB_REPLICA ); |
167 | |
168 | $revWhere = $this->actorMigration->getWhere( $dbr, 'rev_user', $user ); |
169 | $conds = [ |
170 | 'page_latest=rev_id', |
171 | $revWhere['conds'], |
172 | 'page_namespace' => $this->getConfig()->get( 'TranslateMessageNamespaces' ), |
173 | 'page_title' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ), |
174 | ]; |
175 | if ( count( $skipLanguages ) ) { |
176 | $skiplist = $dbr->makeList( $skipLanguages ); |
177 | $conds[] = "substring_index(page_title, '/', -1) NOT IN ($skiplist)"; |
178 | } |
179 | |
180 | $queryInfo = $this->revisionStore->getQueryInfo( [ 'page', 'user' ] ); |
181 | $rows = $dbr->newSelectQueryBuilder() |
182 | ->select( $queryInfo['fields'] ) |
183 | ->tables( $queryInfo['tables'] ) |
184 | ->joinConds( $queryInfo['joins'] ) |
185 | ->where( $conds ) |
186 | ->caller( __METHOD__ ) |
187 | ->fetchResultSet(); |
188 | |
189 | return $this->getMessageContentsFromRows( $rows ); |
190 | } |
191 | |
192 | /** |
193 | * Does the actual edit if possible. |
194 | * @param Title $title |
195 | * @param string $text |
196 | * @param bool $dryrun Whether to really do it or just show what would be done. |
197 | * @param string|null $comment Edit summary. |
198 | */ |
199 | private function updateMessage( Title $title, string $text, bool $dryrun, ?string $comment = null ) { |
200 | $this->output( "Updating {$title->getPrefixedText()}... ", $title ); |
201 | |
202 | $documentationLanguageCode = $this->getConfig()->get( 'TranslateDocumentationLanguageCode' ); |
203 | $items = explode( '/', $title->getText(), 2 ); |
204 | if ( isset( $items[1] ) && $items[1] === $documentationLanguageCode ) { |
205 | $this->output( 'IGNORED!', $title ); |
206 | |
207 | return; |
208 | } |
209 | |
210 | if ( $dryrun ) { |
211 | $this->output( 'DRY RUN!', $title ); |
212 | |
213 | return; |
214 | } |
215 | |
216 | $wikiPage = $this->wikiPageFactory->newFromTitle( $title ); |
217 | $summary = CommentStoreComment::newUnsavedComment( $comment ?? 'Marking as fuzzy' ); |
218 | $content = ContentHandler::makeContent( $text, $title ); |
219 | $updater = $wikiPage->newPageUpdater( FuzzyBot::getUser() ); |
220 | $updater |
221 | ->setContent( SlotRecord::MAIN, $content ) |
222 | ->saveRevision( $summary, EDIT_FORCE_BOT | EDIT_UPDATE ); |
223 | $status = $updater->getStatus(); |
224 | |
225 | $this->output( $status->isOK() ? 'OK' : 'FAILED', $title ); |
226 | } |
227 | } |