Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
39.45% |
43 / 109 |
|
25.00% |
3 / 12 |
CRAP | |
0.00% |
0 / 1 |
MessageGroupReviewStore | |
39.45% |
43 / 109 |
|
25.00% |
3 / 12 |
140.44 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getState | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
changeState | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
2 | |||
getGroupPriority | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setGroupPriority | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
12 | |||
preloadGroupPriorities | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
getWorkflowState | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getWorkflowStatesForLanguage | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getWorkflowStatesForGroup | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getWorkflowStates | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
result2map | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getGroupIdForDatabase | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageGroupProcessing; |
5 | |
6 | use InvalidArgumentException; |
7 | use ManualLogEntry; |
8 | use MediaWiki\Extension\Translate\HookRunner; |
9 | use MediaWiki\SpecialPage\SpecialPage; |
10 | use MediaWiki\User\User; |
11 | use MessageGroup; |
12 | use Wikimedia\Rdbms\IConnectionProvider; |
13 | use Wikimedia\Rdbms\IResultWrapper; |
14 | |
15 | /** |
16 | * Provides methods to get and change the state of a message group |
17 | * @author Eugene Wang'ombe |
18 | * @license GPL-2.0-or-later |
19 | */ |
20 | class MessageGroupReviewStore { |
21 | private HookRunner $hookRunner; |
22 | private IConnectionProvider $dbProvider; |
23 | private const TABLE_NAME = 'translate_groupreviews'; |
24 | /** Cache for message group priorities: (database group id => value) */ |
25 | private ?array $priorityCache = null; |
26 | |
27 | public function __construct( IConnectionProvider $dbProvider, HookRunner $hookRunner ) { |
28 | $this->dbProvider = $dbProvider; |
29 | $this->hookRunner = $hookRunner; |
30 | } |
31 | |
32 | /** @return mixed|false — The value from the field, or false if nothing was found */ |
33 | public function getState( MessageGroup $group, string $code ) { |
34 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
35 | return $dbw->newSelectQueryBuilder() |
36 | ->select( 'tgr_state' ) |
37 | ->from( self::TABLE_NAME ) |
38 | ->where( [ |
39 | 'tgr_group' => self::getGroupIdForDatabase( $group->getId() ), |
40 | 'tgr_lang' => $code |
41 | ] ) |
42 | ->caller( __METHOD__ ) |
43 | ->fetchField(); |
44 | } |
45 | |
46 | /** @return bool true if the message group state changed, otherwise false */ |
47 | public function changeState( MessageGroup $group, string $code, string $newState, User $user ): bool { |
48 | $currentState = $this->getState( $group, $code ); |
49 | if ( $currentState === $newState ) { |
50 | return false; |
51 | } |
52 | |
53 | $row = [ |
54 | 'tgr_group' => self::getGroupIdForDatabase( $group->getId() ), |
55 | 'tgr_lang' => $code, |
56 | 'tgr_state' => $newState, |
57 | ]; |
58 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
59 | $dbw->newReplaceQueryBuilder() |
60 | ->replaceInto( self::TABLE_NAME ) |
61 | ->uniqueIndexFields( [ 'tgr_group', 'tgr_lang' ] ) |
62 | ->row( $row ) |
63 | ->caller( __METHOD__ ) |
64 | ->execute(); |
65 | |
66 | $entry = new ManualLogEntry( 'translationreview', 'group' ); |
67 | $entry->setPerformer( $user ); |
68 | $entry->setTarget( SpecialPage::getTitleFor( 'Translate', $group->getId() ) ); |
69 | $entry->setParameters( [ |
70 | '4::language' => $code, |
71 | '5::group-label' => $group->getLabel(), |
72 | '6::old-state' => $currentState, |
73 | '7::new-state' => $newState, |
74 | ] ); |
75 | // @todo |
76 | // $entry->setComment( $comment ); |
77 | |
78 | $logId = $entry->insert(); |
79 | $entry->publish( $logId ); |
80 | |
81 | $this->hookRunner->onTranslateEventMessageGroupStateChange( $group, $code, $currentState, $newState ); |
82 | |
83 | return true; |
84 | } |
85 | |
86 | public function getGroupPriority( string $group ): ?string { |
87 | $this->preloadGroupPriorities( __METHOD__ ); |
88 | return $this->priorityCache[self::getGroupIdForDatabase( $group )] ?? null; |
89 | } |
90 | |
91 | /** Store priority for message group. Abusing this table that was intended to store message group states */ |
92 | public function setGroupPriority( string $groupId, ?string $priority ): void { |
93 | $dbGroupId = self::getGroupIdForDatabase( $groupId ); |
94 | if ( $this->priorityCache !== null ) { |
95 | $this->priorityCache[$dbGroupId] = $priority; |
96 | } |
97 | |
98 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
99 | $row = [ |
100 | 'tgr_group' => $dbGroupId, |
101 | 'tgr_lang' => '*priority', |
102 | 'tgr_state' => $priority |
103 | ]; |
104 | |
105 | if ( $priority === null ) { |
106 | unset( $row['tgr_state'] ); |
107 | $dbw->newDeleteQueryBuilder() |
108 | ->deleteFrom( self::TABLE_NAME ) |
109 | ->where( $row ) |
110 | ->caller( __METHOD__ ) |
111 | ->execute(); |
112 | } else { |
113 | $dbw->newReplaceQueryBuilder() |
114 | ->replaceInto( self::TABLE_NAME ) |
115 | ->uniqueIndexFields( [ 'tgr_group', 'tgr_lang' ] ) |
116 | ->row( $row ) |
117 | ->caller( __METHOD__ ) |
118 | ->execute(); |
119 | } |
120 | } |
121 | |
122 | private function preloadGroupPriorities( string $caller ): void { |
123 | if ( $this->priorityCache !== null ) { |
124 | return; |
125 | } |
126 | |
127 | $dbr = $this->dbProvider->getReplicaDatabase(); |
128 | $res = $dbr->newSelectQueryBuilder() |
129 | ->select( [ 'tgr_group', 'tgr_state' ] ) |
130 | ->from( self::TABLE_NAME ) |
131 | ->where( [ 'tgr_lang' => '*priority' ] ) |
132 | ->caller( $caller ) |
133 | ->fetchResultSet(); |
134 | |
135 | $this->priorityCache = $this->result2map( $res, 'tgr_group', 'tgr_state' ); |
136 | } |
137 | |
138 | /** |
139 | * Get the current workflow state for the given message group for the given language |
140 | * @param string $groupId |
141 | * @param string $languageCode |
142 | * @return string|null State id or null. |
143 | */ |
144 | public function getWorkflowState( string $groupId, string $languageCode ): ?string { |
145 | $result = $this->getWorkflowStates( [ $groupId ], [ $languageCode ] ); |
146 | return $result->fetchRow()['tgr_state'] ?? null; |
147 | } |
148 | |
149 | public function getWorkflowStatesForLanguage( string $languageCode, array $groupIds ): array { |
150 | $result = $this->getWorkflowStates( $groupIds, [ $languageCode ] ); |
151 | $states = $this->result2map( $result, 'tgr_group', 'tgr_state' ); |
152 | |
153 | $finalResult = []; |
154 | foreach ( $groupIds as $groupId ) { |
155 | $dbGroupId = self::getGroupIdForDatabase( $groupId ); |
156 | if ( isset( $states[ $dbGroupId ] ) ) { |
157 | $finalResult[ $groupId ] = $states[ $dbGroupId ]; |
158 | } |
159 | } |
160 | |
161 | return $finalResult; |
162 | } |
163 | |
164 | public function getWorkflowStatesForGroup( string $groupId ): array { |
165 | $result = $this->getWorkflowStates( [ $groupId ], null ); |
166 | return $this->result2map( $result, 'tgr_lang', 'tgr_state' ); |
167 | } |
168 | |
169 | private function getWorkflowStates( ?array $groupIds, ?array $languageCodes ): IResultWrapper { |
170 | $dbr = $this->dbProvider->getReplicaDatabase(); |
171 | $conditions = array_filter( |
172 | [ 'tgr_group' => $groupIds, 'tgr_lang' => $languageCodes ], |
173 | static fn ( $x ) => $x !== null && $x !== '' |
174 | ); |
175 | |
176 | if ( $conditions === [] ) { |
177 | throw new InvalidArgumentException( 'Either the $groupId or the $languageCode should be provided' ); |
178 | } |
179 | |
180 | if ( isset( $conditions['tgr_group'] ) ) { |
181 | $conditions['tgr_group'] = array_map( [ self::class, 'getGroupIdForDatabase' ], $groupIds ); |
182 | } |
183 | |
184 | return $dbr->newSelectQueryBuilder() |
185 | ->select( [ 'tgr_state', 'tgr_group', 'tgr_lang' ] ) |
186 | ->from( self::TABLE_NAME ) |
187 | ->where( $conditions ) |
188 | ->caller( __METHOD__ ) |
189 | ->fetchResultSet(); |
190 | } |
191 | |
192 | private function result2map( IResultWrapper $result, string $keyValue, string $valueValue ): array { |
193 | $map = []; |
194 | foreach ( $result as $row ) { |
195 | $map[$row->$keyValue] = $row->$valueValue; |
196 | } |
197 | |
198 | return $map; |
199 | } |
200 | |
201 | private static function getGroupIdForDatabase( $groupId ): string { |
202 | $groupId = strval( $groupId ); |
203 | |
204 | // Check if length is more than 200 bytes |
205 | if ( strlen( $groupId ) <= 200 ) { |
206 | return $groupId; |
207 | } |
208 | |
209 | // We take 160 bytes of the original string and append the md5 hash (32 bytes) |
210 | return mb_strcut( $groupId, 0, 160 ) . '||' . hash( 'md5', $groupId ); |
211 | } |
212 | } |