Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 117 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
MessageGroupMetadata | |
0.00% |
0 / 117 |
|
0.00% |
0 / 14 |
1122 | |
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 / 15 |
|
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 / 6 |
|
0.00% |
0 / 1 |
2 | |||
isExcluded | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
30 | |||
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\ILoadBalancer; |
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 ILoadBalancer $loadBalancer; |
26 | |
27 | public function __construct( ILoadBalancer $loadBalancer ) { |
28 | $this->loadBalancer = $loadBalancer; |
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->loadBalancer->getConnection( DB_PRIMARY ); |
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->delete( 'translate_metadata', $data, __METHOD__ ); |
92 | unset( $this->cache[$dbGroupId][$key] ); |
93 | } else { |
94 | $dbw->replace( |
95 | 'translate_metadata', |
96 | [ [ 'tmd_group', 'tmd_key' ] ], |
97 | $data, |
98 | __METHOD__ |
99 | ); |
100 | $this->cache[$dbGroupId][$key] = $value; |
101 | } |
102 | |
103 | $this->priorityCache = null; |
104 | } |
105 | |
106 | /** |
107 | * Wrapper for getting subgroups. |
108 | * @return string[]|null |
109 | */ |
110 | public function getSubgroups( string $groupId ): ?array { |
111 | $groups = $this->get( $groupId, 'subgroups' ); |
112 | if ( is_string( $groups ) ) { |
113 | if ( str_contains( $groups, '|' ) ) { |
114 | $groups = explode( '|', $groups ); |
115 | } else { |
116 | $groups = array_map( 'trim', explode( ',', $groups ) ); |
117 | } |
118 | |
119 | foreach ( $groups as $index => $id ) { |
120 | if ( trim( $id ) === '' ) { |
121 | unset( $groups[$index] ); |
122 | } |
123 | } |
124 | } else { |
125 | $groups = null; |
126 | } |
127 | |
128 | return $groups; |
129 | } |
130 | |
131 | /** Wrapper for setting subgroups. */ |
132 | public function setSubgroups( string $groupId, array $subgroupIds ): void { |
133 | $subgroups = implode( '|', $subgroupIds ); |
134 | $this->set( $groupId, 'subgroups', $subgroups ); |
135 | } |
136 | |
137 | /** Wrapper for deleting one wiki aggregate group at once. */ |
138 | public function deleteGroup( string $groupId ): void { |
139 | $dbw = $this->loadBalancer->getConnection( DB_PRIMARY ); |
140 | |
141 | $dbGroupId = $this->getGroupIdForDatabase( $groupId ); |
142 | $conditions = [ 'tmd_group' => $dbGroupId ]; |
143 | $dbw->delete( 'translate_metadata', $conditions, __METHOD__ ); |
144 | $this->cache[ $dbGroupId ] = null; |
145 | unset( $this->priorityCache[ $dbGroupId ] ); |
146 | } |
147 | |
148 | public function isExcluded( string $groupId, string $code ): bool { |
149 | if ( $this->priorityCache === null ) { |
150 | // TODO: Ideally, this should use the injected ILoadBalancer to make it mockable. |
151 | $db = Utilities::getSafeReadDB(); |
152 | $res = $db->newSelectQueryBuilder() |
153 | ->select( [ |
154 | 'group' => 'b.tmd_group', |
155 | 'langs' => 'b.tmd_value', |
156 | ] ) |
157 | ->from( 'translate_metadata', 'a' ) |
158 | ->join( 'translate_metadata', 'b', [ |
159 | 'a.tmd_group = b.tmd_group', |
160 | 'a.tmd_key' => 'priorityforce', |
161 | 'a.tmd_value' => 'on', |
162 | 'b.tmd_key' => 'prioritylangs', |
163 | ] ) |
164 | ->caller( __METHOD__ ) |
165 | ->fetchResultSet(); |
166 | |
167 | $this->priorityCache = []; |
168 | foreach ( $res as $row ) { |
169 | $this->priorityCache[$row->group] = |
170 | array_flip( explode( ',', $row->langs ) ); |
171 | } |
172 | } |
173 | |
174 | $dbGroupId = $this->getGroupIdForDatabase( $groupId ); |
175 | $isDiscouraged = MessageGroups::getPriority( $groupId ) === 'discouraged'; |
176 | $hasLimitedLanguages = isset( $this->priorityCache[$dbGroupId] ); |
177 | $isLanguageIncluded = isset( $this->priorityCache[$dbGroupId][$code] ); |
178 | |
179 | return $isDiscouraged || ( $hasLimitedLanguages && !$isLanguageIncluded ); |
180 | } |
181 | |
182 | /** |
183 | * Do a query optimized for page list in Special:PageTranslation |
184 | * @param string[] $groupIds |
185 | * @param string[] $keys Which metadata keys to load |
186 | * @return array<string,array<string,string>> |
187 | */ |
188 | public function loadBasicMetadataForTranslatablePages( array $groupIds, array $keys ): array { |
189 | // TODO: Ideally, this should use the injected ILoadBalancer to make it mockable. |
190 | $db = Utilities::getSafeReadDB(); |
191 | $dbGroupIdMap = []; |
192 | |
193 | foreach ( $groupIds as $groupId ) { |
194 | $dbGroupIdMap[ $this->getGroupIdForDatabase( $groupId ) ] = $groupId; |
195 | } |
196 | |
197 | $res = $db->newSelectQueryBuilder() |
198 | ->select( [ 'tmd_group', 'tmd_key', 'tmd_value' ] ) |
199 | ->from( 'translate_metadata' ) |
200 | ->where( [ |
201 | 'tmd_group' => array_keys( $dbGroupIdMap ), |
202 | 'tmd_key' => $keys, |
203 | ] ) |
204 | ->caller( __METHOD__ ) |
205 | ->fetchResultSet(); |
206 | |
207 | $ret = []; |
208 | foreach ( $res as $row ) { |
209 | $groupId = $row->tmd_group; |
210 | // Remap the db group ids to group id in the response |
211 | $ret[ $dbGroupIdMap[ $groupId ] ][ $row->tmd_key ] = $row->tmd_value; |
212 | } |
213 | |
214 | return $ret; |
215 | } |
216 | |
217 | public function moveMetadata( |
218 | string $oldGroupId, |
219 | string $newGroupId, |
220 | array $metadataKeysToMove |
221 | ): void { |
222 | $this->preloadGroups( [ $oldGroupId, $newGroupId ], __METHOD__ ); |
223 | foreach ( $metadataKeysToMove as $type ) { |
224 | $value = $this->get( $oldGroupId, $type ); |
225 | if ( $value !== false ) { |
226 | $this->set( $oldGroupId, $type, false ); |
227 | $this->set( $newGroupId, $type, $value ); |
228 | } |
229 | } |
230 | } |
231 | |
232 | /** |
233 | * @param string $groupId |
234 | * @param string[] $metadataKeys |
235 | */ |
236 | public function clearMetadata( string $groupId, array $metadataKeys ): void { |
237 | // remove the entries from metadata table. |
238 | foreach ( $metadataKeys as $type ) { |
239 | $this->set( $groupId, $type, false ); |
240 | } |
241 | } |
242 | |
243 | /** Get groups ids that have subgroups set up. */ |
244 | public function getGroupsWithSubgroups(): array { |
245 | // TODO: Ideally, this should use the injected ILoadBalancer to make it mockable. |
246 | $db = Utilities::getSafeReadDB(); |
247 | // There is no need to de-hash the group id from the database as |
248 | // AggregateGroupsActionApi::generateAggregateGroupId already ensures that the length |
249 | // is appropriate |
250 | return $db->newSelectQueryBuilder() |
251 | ->select( 'tmd_group' ) |
252 | ->from( 'translate_metadata' ) |
253 | ->where( [ 'tmd_key' => 'subgroups' ] ) |
254 | ->caller( __METHOD__ ) |
255 | ->fetchFieldValues(); |
256 | } |
257 | |
258 | private function getGroupIdForDatabase( string $groupId ): string { |
259 | // Check if length is more than 200 bytes |
260 | if ( strlen( $groupId ) <= 200 ) { |
261 | return $groupId; |
262 | } |
263 | |
264 | $hash = hash( 'md5', $groupId ); |
265 | // We take 160 bytes of the original string and append the md5 hash (32 bytes) |
266 | return mb_strcut( $groupId, 0, 160 ) . '||' . $hash; |
267 | } |
268 | } |