Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 127 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
MessageGroupMetadata | |
0.00% |
0 / 127 |
|
0.00% |
0 / 14 |
1190 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
preloadGroups | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
get | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getWithDefaultValue | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
set | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
6 | |||
getSubgroups | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
setSubgroups | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
deleteGroup | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
isExcluded | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
42 | |||
loadBasicMetadataForTranslatablePages | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
moveMetadata | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
clearMetadata | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getGroupsWithSubgroups | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getGroupIdForDatabase | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageProcessing; |
5 | |
6 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups; |
7 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
8 | use Wikimedia\Rdbms\IConnectionProvider; |
9 | |
10 | /** |
11 | * Offers functionality for reading and updating Translate group |
12 | * related metadata |
13 | * |
14 | * @author Niklas Laxström |
15 | * @author Santhosh Thottingal |
16 | * @copyright Copyright © 2012-2013, Niklas Laxström, Santhosh Thottingal |
17 | * @license GPL-2.0-or-later |
18 | */ |
19 | class MessageGroupMetadata { |
20 | /** @var int Threshold for query batching */ |
21 | private const MAX_ITEMS_PER_QUERY = 2000; |
22 | /** Map of (database group id => key => value) */ |
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 | |
58 | /** |
59 | * Get a metadata value for the given group and key. |
60 | * @param string $group The group name |
61 | * @param string $key Metadata key |
62 | * @return string|bool |
63 | */ |
64 | public function get( string $group, string $key ) { |
65 | $this->preloadGroups( [ $group ], __METHOD__ ); |
66 | return $this->cache[$this->getGroupIdForDatabase( $group )][$key] ?? false; |
67 | } |
68 | |
69 | /** |
70 | * Get a metadata value for the given group and key. |
71 | * If it does not exist, return the default value. |
72 | */ |
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 | |
78 | /** |
79 | * Set a metadata value for the given group and metadata key. Updates the |
80 | * value if already existing. |
81 | * @param string $groupId The group id |
82 | * @param string $key Metadata key |
83 | * @param string|false $value Metadata value, false deletes from cache |
84 | */ |
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 | |
110 | /** |
111 | * Wrapper for getting subgroups. |
112 | * @return string[]|null |
113 | */ |
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 | |
135 | /** Wrapper for setting subgroups. */ |
136 | public function setSubgroups( string $groupId, array $subgroupIds ): void { |
137 | $subgroups = implode( '|', $subgroupIds ); |
138 | $this->set( $groupId, 'subgroups', $subgroups ); |
139 | } |
140 | |
141 | /** Wrapper for deleting one wiki aggregate group at once. */ |
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 | |
194 | /** |
195 | * Do a query optimized for page list in Special:PageTranslation |
196 | * @param string[] $groupIds |
197 | * @param string[] $keys Which metadata keys to load |
198 | * @return array<string,array<string,string>> |
199 | */ |
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 | |
244 | /** |
245 | * @param string $groupId |
246 | * @param string[] $metadataKeys |
247 | */ |
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 | |
255 | /** Get groups ids that have subgroups set up. */ |
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 | } |