Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
39.45% covered (danger)
39.45%
43 / 109
25.00% covered (danger)
25.00%
3 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
MessageGroupReviewStore
39.45% covered (danger)
39.45%
43 / 109
25.00% covered (danger)
25.00%
3 / 12
140.44
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getState
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 changeState
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
2
 getGroupPriority
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setGroupPriority
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 preloadGroupPriorities
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getWorkflowState
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getWorkflowStatesForLanguage
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getWorkflowStatesForGroup
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getWorkflowStates
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 result2map
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getGroupIdForDatabase
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use InvalidArgumentException;
7use ManualLogEntry;
8use MediaWiki\Extension\Translate\HookRunner;
9use MediaWiki\SpecialPage\SpecialPage;
10use MediaWiki\User\User;
11use MessageGroup;
12use Wikimedia\Rdbms\IConnectionProvider;
13use 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 */
20class 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}