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
26 public function __construct( private readonly IConnectionProvider $dbProvider ) {
27 }
28
29 public function preloadGroups( array $groups, string $caller ): void {
30 $dbGroupIds = array_map( $this->getGroupIdForDatabase( ... ), $groups );
31 $missing = array_keys( array_diff_key( array_flip( $dbGroupIds ), $this->cache ) );
32 if ( !$missing ) {
33 return;
34 }
35
36 $functionName = __METHOD__ . " (for $caller)";
37
38 $this->cache += array_fill_keys( $missing, null ); // cache negatives
39
40 // TODO: Ideally, this should use the injected ILoadBalancer to make it mockable.
41 $dbr = Utilities::getSafeReadDB();
42 $chunks = array_chunk( $missing, self::MAX_ITEMS_PER_QUERY );
43 foreach ( $chunks as $chunk ) {
44 $res = $dbr->newSelectQueryBuilder()
45 ->select( [ 'tmd_group', 'tmd_key', 'tmd_value' ] )
46 ->from( 'translate_metadata' )
47 ->where( [ 'tmd_group' => array_map( strval( ... ), $chunk ) ] )
48 ->caller( $functionName )
49 ->fetchResultSet();
50 foreach ( $res as $row ) {
51 $this->cache[$row->tmd_group][$row->tmd_key] = $row->tmd_value;
52 }
53 }
54 }
55
61 public function get( string $group, string $key ): string|false {
62 $this->preloadGroups( [ $group ], __METHOD__ );
63 return $this->cache[$this->getGroupIdForDatabase( $group )][$key] ?? false;
64 }
65
70 public function getWithDefaultValue( string $group, string $key, ?string $defaultValue ): ?string {
71 $value = $this->get( $group, $key );
72 return $value === false ? $defaultValue : $value;
73 }
74
82 public function set( string $groupId, string $key, string|false $value ): void {
83 $dbw = $this->dbProvider->getPrimaryDatabase();
84 $dbGroupId = $this->getGroupIdForDatabase( $groupId );
85 $data = [ 'tmd_group' => $dbGroupId, 'tmd_key' => $key, 'tmd_value' => $value ];
86 if ( $value === false ) {
87 unset( $data['tmd_value'] );
88 $dbw->newDeleteQueryBuilder()
89 ->deleteFrom( 'translate_metadata' )
90 ->where( $data )
91 ->caller( __METHOD__ )
92 ->execute();
93 unset( $this->cache[$dbGroupId][$key] );
94 } else {
95 $dbw->newReplaceQueryBuilder()
96 ->replaceInto( 'translate_metadata' )
97 ->uniqueIndexFields( [ 'tmd_group', 'tmd_key' ] )
98 ->row( $data )
99 ->caller( __METHOD__ )
100 ->execute();
101 $this->cache[$dbGroupId][$key] = $value;
102 }
103
104 $this->priorityCache = null;
105 }
106
111 public function getSubgroups( string $groupId ): ?array {
112 $groups = $this->get( $groupId, 'subgroups' );
113 if ( is_string( $groups ) ) {
114 if ( str_contains( $groups, '|' ) ) {
115 $groups = explode( '|', $groups );
116 } else {
117 $groups = array_map( 'trim', explode( ',', $groups ) );
118 }
119
120 foreach ( $groups as $index => $id ) {
121 if ( trim( $id ) === '' ) {
122 unset( $groups[$index] );
123 }
124 }
125 } else {
126 $groups = null;
127 }
128
129 return $groups;
130 }
131
133 public function setSubgroups( string $groupId, array $subgroupIds ): void {
134 $subgroups = implode( '|', $subgroupIds );
135 $this->set( $groupId, 'subgroups', $subgroups );
136 }
137
139 public function deleteGroup( string $groupId ): void {
140 $dbw = $this->dbProvider->getPrimaryDatabase();
141
142 $dbGroupId = $this->getGroupIdForDatabase( $groupId );
143 $dbw->newDeleteQueryBuilder()
144 ->deleteFrom( 'translate_metadata' )
145 ->where( [ 'tmd_group' => $dbGroupId ] )
146 ->caller( __METHOD__ )
147 ->execute();
148 $this->cache[ $dbGroupId ] = null;
149 unset( $this->priorityCache[ $dbGroupId ] );
150 }
151
152 public function isExcluded( string $groupId, string $code ): bool {
153 if ( $this->priorityCache === null ) {
154 // TODO: Ideally, this should use the injected ILoadBalancer to make it mockable.
155 $db = Utilities::getSafeReadDB();
156 $res = $db->newSelectQueryBuilder()
157 ->select( [
158 'group' => 'a.tmd_group',
159 'langs' => 'b.tmd_value',
160 ] )
161 ->from( 'translate_metadata', 'a' )
162 ->leftJoin( 'translate_metadata', 'b', [
163 'a.tmd_group = b.tmd_group',
164 'b.tmd_key' => 'prioritylangs',
165 ] )
166 ->where( [
167 'a.tmd_key' => 'priorityforce',
168 'a.tmd_value' => 'on'
169 ] )
170 ->caller( __METHOD__ )
171 ->fetchResultSet();
172
173 $this->priorityCache = [];
174 foreach ( $res as $row ) {
175 $this->priorityCache[$row->group] = isset( $row->langs )
176 ? array_flip( explode( ',', $row->langs ) )
177 : [];
178 }
179 }
180
181 $dbGroupId = $this->getGroupIdForDatabase( $groupId );
182 $isDiscouraged = MessageGroups::getPriority( $groupId ) === 'discouraged';
183 $hasLimitedLanguages = isset( $this->priorityCache[$dbGroupId] );
184 $isLanguageIncluded = isset( $this->priorityCache[$dbGroupId][$code] );
185
186 return $isDiscouraged || ( $hasLimitedLanguages && !$isLanguageIncluded );
187 }
188
195 public function loadBasicMetadataForTranslatablePages( array $groupIds, array $keys ): array {
196 // TODO: Ideally, this should use the injected ILoadBalancer to make it mockable.
197 $db = Utilities::getSafeReadDB();
198 $dbGroupIdMap = [];
199
200 foreach ( $groupIds as $groupId ) {
201 $dbGroupIdMap[ $this->getGroupIdForDatabase( $groupId ) ] = $groupId;
202 }
203
204 $res = $db->newSelectQueryBuilder()
205 ->select( [ 'tmd_group', 'tmd_key', 'tmd_value' ] )
206 ->from( 'translate_metadata' )
207 ->where( [
208 'tmd_group' => array_keys( $dbGroupIdMap ),
209 'tmd_key' => $keys,
210 ] )
211 ->caller( __METHOD__ )
212 ->fetchResultSet();
213
214 $ret = [];
215 foreach ( $res as $row ) {
216 $groupId = $row->tmd_group;
217 // Remap the db group ids to group id in the response
218 $ret[ $dbGroupIdMap[ $groupId ] ][ $row->tmd_key ] = $row->tmd_value;
219 }
220
221 return $ret;
222 }
223
224 public function moveMetadata(
225 string $oldGroupId,
226 string $newGroupId,
227 array $metadataKeysToMove
228 ): void {
229 $this->preloadGroups( [ $oldGroupId, $newGroupId ], __METHOD__ );
230 foreach ( $metadataKeysToMove as $type ) {
231 $value = $this->get( $oldGroupId, $type );
232 if ( $value !== false ) {
233 $this->set( $oldGroupId, $type, false );
234 $this->set( $newGroupId, $type, $value );
235 }
236 }
237 }
238
243 public function clearMetadata( string $groupId, array $metadataKeys ): void {
244 // remove the entries from metadata table.
245 foreach ( $metadataKeys as $type ) {
246 $this->set( $groupId, $type, false );
247 }
248 }
249
251 public function getGroupsWithSubgroups(): array {
252 // TODO: Ideally, this should use the injected ILoadBalancer to make it mockable.
253 $db = Utilities::getSafeReadDB();
254 // There is no need to de-hash the group id from the database as
255 // AggregateGroupsActionApi::generateAggregateGroupId already ensures that the length
256 // is appropriate
257 return $db->newSelectQueryBuilder()
258 ->select( 'tmd_group' )
259 ->from( 'translate_metadata' )
260 ->where( [ 'tmd_key' => 'subgroups' ] )
261 ->caller( __METHOD__ )
262 ->fetchFieldValues();
263 }
264
265 private function getGroupIdForDatabase( string $groupId ): string {
266 // Check if length is more than 200 bytes
267 if ( strlen( $groupId ) <= 200 ) {
268 return $groupId;
269 }
270
271 $hash = hash( 'md5', $groupId );
272 // We take 160 bytes of the original string and append the md5 hash (32 bytes)
273 return mb_strcut( $groupId, 0, 160 ) . '||' . $hash;
274 }
275}
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.
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:29