Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
MessageGroupMetadata.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageProcessing;
5
8use Wikimedia\Rdbms\IConnectionProvider;
9
21 private const MAX_ITEMS_PER_QUERY = 2000;
23 private array $cache = [];
24 private ?array $priorityCache = null;
25 private IConnectionProvider $dbProvider;
26
27 public function __construct( IConnectionProvider $dbProvider ) {
28 $this->dbProvider = $dbProvider;
29 }
30
31 public function preloadGroups( array $groups, string $caller ): void {
32 $dbGroupIds = array_map( [ $this, 'getGroupIdForDatabase' ], $groups );
33 $missing = array_keys( array_diff_key( array_flip( $dbGroupIds ), $this->cache ) );
34 if ( !$missing ) {
35 return;
36 }
37
38 $functionName = __METHOD__ . " (for $caller)";
39
40 $this->cache += array_fill_keys( $missing, null ); // cache negatives
41
42 // TODO: Ideally, this should use the injected ILoadBalancer to make it mockable.
43 $dbr = Utilities::getSafeReadDB();
44 $chunks = array_chunk( $missing, self::MAX_ITEMS_PER_QUERY );
45 foreach ( $chunks as $chunk ) {
46 $res = $dbr->newSelectQueryBuilder()
47 ->select( [ 'tmd_group', 'tmd_key', 'tmd_value' ] )
48 ->from( 'translate_metadata' )
49 ->where( [ 'tmd_group' => array_map( 'strval', $chunk ) ] )
50 ->caller( $functionName )
51 ->fetchResultSet();
52 foreach ( $res as $row ) {
53 $this->cache[$row->tmd_group][$row->tmd_key] = $row->tmd_value;
54 }
55 }
56 }
57
64 public function get( string $group, string $key ) {
65 $this->preloadGroups( [ $group ], __METHOD__ );
66 return $this->cache[$this->getGroupIdForDatabase( $group )][$key] ?? false;
67 }
68
73 public function getWithDefaultValue( string $group, string $key, ?string $defaultValue ): ?string {
74 $value = $this->get( $group, $key );
75 return $value === false ? $defaultValue : $value;
76 }
77
85 public function set( string $groupId, string $key, $value ): void {
86 $dbw = $this->dbProvider->getPrimaryDatabase();
87 $dbGroupId = $this->getGroupIdForDatabase( $groupId );
88 $data = [ 'tmd_group' => $dbGroupId, 'tmd_key' => $key, 'tmd_value' => $value ];
89 if ( $value === false ) {
90 unset( $data['tmd_value'] );
91 $dbw->newDeleteQueryBuilder()
92 ->deleteFrom( 'translate_metadata' )
93 ->where( $data )
94 ->caller( __METHOD__ )
95 ->execute();
96 unset( $this->cache[$dbGroupId][$key] );
97 } else {
98 $dbw->newReplaceQueryBuilder()
99 ->replaceInto( 'translate_metadata' )
100 ->uniqueIndexFields( [ 'tmd_group', 'tmd_key' ] )
101 ->row( $data )
102 ->caller( __METHOD__ )
103 ->execute();
104 $this->cache[$dbGroupId][$key] = $value;
105 }
106
107 $this->priorityCache = null;
108 }
109
114 public function getSubgroups( string $groupId ): ?array {
115 $groups = $this->get( $groupId, 'subgroups' );
116 if ( is_string( $groups ) ) {
117 if ( str_contains( $groups, '|' ) ) {
118 $groups = explode( '|', $groups );
119 } else {
120 $groups = array_map( 'trim', explode( ',', $groups ) );
121 }
122
123 foreach ( $groups as $index => $id ) {
124 if ( trim( $id ) === '' ) {
125 unset( $groups[$index] );
126 }
127 }
128 } else {
129 $groups = null;
130 }
131
132 return $groups;
133 }
134
136 public function setSubgroups( string $groupId, array $subgroupIds ): void {
137 $subgroups = implode( '|', $subgroupIds );
138 $this->set( $groupId, 'subgroups', $subgroups );
139 }
140
142 public function deleteGroup( string $groupId ): void {
143 $dbw = $this->dbProvider->getPrimaryDatabase();
144
145 $dbGroupId = $this->getGroupIdForDatabase( $groupId );
146 $dbw->newDeleteQueryBuilder()
147 ->deleteFrom( 'translate_metadata' )
148 ->where( [ 'tmd_group' => $dbGroupId ] )
149 ->caller( __METHOD__ )
150 ->execute();
151 $this->cache[ $dbGroupId ] = null;
152 unset( $this->priorityCache[ $dbGroupId ] );
153 }
154
155 public function isExcluded( string $groupId, string $code ): bool {
156 if ( $this->priorityCache === null ) {
157 // TODO: Ideally, this should use the injected ILoadBalancer to make it mockable.
158 $db = Utilities::getSafeReadDB();
159 $res = $db->newSelectQueryBuilder()
160 ->select( [
161 'group' => 'a.tmd_group',
162 'langs' => 'b.tmd_value',
163 ] )
164 ->from( 'translate_metadata', 'a' )
165 ->leftJoin( 'translate_metadata', 'b', [
166 'a.tmd_group = b.tmd_group',
167 'b.tmd_key' => 'prioritylangs',
168 ] )
169 ->where( [
170 'a.tmd_key' => 'priorityforce',
171 'a.tmd_value' => 'on'
172 ] )
173 ->caller( __METHOD__ )
174 ->fetchResultSet();
175
176 $this->priorityCache = [];
177 foreach ( $res as $row ) {
178 if ( isset( $row->langs ) ) {
179 $this->priorityCache[ $row->group ] = array_flip( explode( ',', $row->langs ) );
180 } else {
181 $this->priorityCache[ $row->group ] = [];
182 }
183 }
184 }
185
186 $dbGroupId = $this->getGroupIdForDatabase( $groupId );
187 $isDiscouraged = MessageGroups::getPriority( $groupId ) === 'discouraged';
188 $hasLimitedLanguages = isset( $this->priorityCache[$dbGroupId] );
189 $isLanguageIncluded = isset( $this->priorityCache[$dbGroupId][$code] );
190
191 return $isDiscouraged || ( $hasLimitedLanguages && !$isLanguageIncluded );
192 }
193
200 public function loadBasicMetadataForTranslatablePages( array $groupIds, array $keys ): array {
201 // TODO: Ideally, this should use the injected ILoadBalancer to make it mockable.
202 $db = Utilities::getSafeReadDB();
203 $dbGroupIdMap = [];
204
205 foreach ( $groupIds as $groupId ) {
206 $dbGroupIdMap[ $this->getGroupIdForDatabase( $groupId ) ] = $groupId;
207 }
208
209 $res = $db->newSelectQueryBuilder()
210 ->select( [ 'tmd_group', 'tmd_key', 'tmd_value' ] )
211 ->from( 'translate_metadata' )
212 ->where( [
213 'tmd_group' => array_keys( $dbGroupIdMap ),
214 'tmd_key' => $keys,
215 ] )
216 ->caller( __METHOD__ )
217 ->fetchResultSet();
218
219 $ret = [];
220 foreach ( $res as $row ) {
221 $groupId = $row->tmd_group;
222 // Remap the db group ids to group id in the response
223 $ret[ $dbGroupIdMap[ $groupId ] ][ $row->tmd_key ] = $row->tmd_value;
224 }
225
226 return $ret;
227 }
228
229 public function moveMetadata(
230 string $oldGroupId,
231 string $newGroupId,
232 array $metadataKeysToMove
233 ): void {
234 $this->preloadGroups( [ $oldGroupId, $newGroupId ], __METHOD__ );
235 foreach ( $metadataKeysToMove as $type ) {
236 $value = $this->get( $oldGroupId, $type );
237 if ( $value !== false ) {
238 $this->set( $oldGroupId, $type, false );
239 $this->set( $newGroupId, $type, $value );
240 }
241 }
242 }
243
248 public function clearMetadata( string $groupId, array $metadataKeys ): void {
249 // remove the entries from metadata table.
250 foreach ( $metadataKeys as $type ) {
251 $this->set( $groupId, $type, false );
252 }
253 }
254
256 public function getGroupsWithSubgroups(): array {
257 // TODO: Ideally, this should use the injected ILoadBalancer to make it mockable.
258 $db = Utilities::getSafeReadDB();
259 // There is no need to de-hash the group id from the database as
260 // AggregateGroupsActionApi::generateAggregateGroupId already ensures that the length
261 // is appropriate
262 return $db->newSelectQueryBuilder()
263 ->select( 'tmd_group' )
264 ->from( 'translate_metadata' )
265 ->where( [ 'tmd_key' => 'subgroups' ] )
266 ->caller( __METHOD__ )
267 ->fetchFieldValues();
268 }
269
270 private function getGroupIdForDatabase( string $groupId ): string {
271 // Check if length is more than 200 bytes
272 if ( strlen( $groupId ) <= 200 ) {
273 return $groupId;
274 }
275
276 $hash = hash( 'md5', $groupId );
277 // We take 160 bytes of the original string and append the md5 hash (32 bytes)
278 return mb_strcut( $groupId, 0, 160 ) . '||' . $hash;
279 }
280}
Factory class for accessing message groups individually by id or all of them as a list.
Offers functionality for reading and updating Translate group related metadata.
getWithDefaultValue(string $group, string $key, ?string $defaultValue)
Get a metadata value for the given group and key.
loadBasicMetadataForTranslatablePages(array $groupIds, array $keys)
Do a query optimized for page list in Special:PageTranslation.
setSubgroups(string $groupId, array $subgroupIds)
Wrapper for setting subgroups.
get(string $group, string $key)
Get a metadata value for the given group and key.
deleteGroup(string $groupId)
Wrapper for deleting one wiki aggregate group at once.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31