Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
38.95% covered (danger)
38.95%
37 / 95
25.00% covered (danger)
25.00%
3 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
MessageGroupReviewStore
38.95% covered (danger)
38.95%
37 / 95
25.00% covered (danger)
25.00%
3 / 12
143.38
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%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 changeState
100.00% covered (success)
100.00%
24 / 24
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 / 14
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 MessageGroup;
10use SpecialPage;
11use User;
12use Wikimedia\Rdbms\ILoadBalancer;
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 ILoadBalancer $loadBalancer;
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( ILoadBalancer $loadBalancer, HookRunner $hookRunner ) {
28        $this->loadBalancer = $loadBalancer;
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->loadBalancer->getConnection( DB_PRIMARY );
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            ] )->fetchField();
42    }
43
44    /** @return bool true if the message group state changed, otherwise false */
45    public function changeState( MessageGroup $group, string $code, string $newState, User $user ): bool {
46        $currentState = $this->getState( $group, $code );
47        if ( $currentState === $newState ) {
48            return false;
49        }
50
51        $index = [ 'tgr_group', 'tgr_lang' ];
52        $row = [
53            'tgr_group' => self::getGroupIdForDatabase( $group->getId() ),
54            'tgr_lang' => $code,
55            'tgr_state' => $newState,
56        ];
57        $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
58        $dbw->replace( self::TABLE_NAME, [ $index ], $row, __METHOD__ );
59
60        $entry = new ManualLogEntry( 'translationreview', 'group' );
61        $entry->setPerformer( $user );
62        $entry->setTarget( SpecialPage::getTitleFor( 'Translate', $group->getId() ) );
63        $entry->setParameters( [
64            '4::language' => $code,
65            '5::group-label' => $group->getLabel(),
66            '6::old-state' => $currentState,
67            '7::new-state' => $newState,
68        ] );
69        // @todo
70        // $entry->setComment( $comment );
71
72        $logId = $entry->insert();
73        $entry->publish( $logId );
74
75        $this->hookRunner->onTranslateEventMessageGroupStateChange( $group, $code, $currentState, $newState );
76
77        return true;
78    }
79
80    public function getGroupPriority( string $group ): ?string {
81        $this->preloadGroupPriorities( __METHOD__ );
82        return $this->priorityCache[self::getGroupIdForDatabase( $group )] ?? null;
83    }
84
85    /** Store priority for message group. Abusing this table that was intended to store message group states */
86    public function setGroupPriority( string $groupId, ?string $priority ): void {
87        $dbGroupId = self::getGroupIdForDatabase( $groupId );
88        if ( isset( $this->priorityCache ) ) {
89            $this->priorityCache[$dbGroupId] = $priority;
90        }
91
92        $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
93        $row = [
94            'tgr_group' => $dbGroupId,
95            'tgr_lang' => '*priority',
96            'tgr_state' => $priority
97        ];
98
99        if ( $priority === null ) {
100            unset( $row['tgr_state'] );
101            $dbw->delete( self::TABLE_NAME, $row, __METHOD__ );
102        } else {
103            $index = [ 'tgr_group', 'tgr_lang' ];
104            $dbw->replace( self::TABLE_NAME, [ $index ], $row, __METHOD__ );
105        }
106    }
107
108    private function preloadGroupPriorities( string $caller ): void {
109        if ( isset( $this->priorityCache ) ) {
110            return;
111        }
112
113        $dbr = $this->loadBalancer->getConnection( DB_REPLICA );
114        $res = $dbr->newSelectQueryBuilder()
115            ->select( [ 'tgr_group', 'tgr_state' ] )
116            ->from( self::TABLE_NAME )
117            ->where( [ 'tgr_lang' => '*priority' ] )
118            ->caller( $caller )
119            ->fetchResultSet();
120
121        $this->priorityCache = $this->result2map( $res, 'tgr_group', 'tgr_state' );
122    }
123
124    /**
125     * Get the current workflow state for the given message group for the given language
126     * @param string $groupId
127     * @param string $languageCode
128     * @return string|null State id or null.
129     */
130    public function getWorkflowState( string $groupId, string $languageCode ): ?string {
131        $result = $this->getWorkflowStates( [ $groupId ], [ $languageCode ] );
132        return $result->fetchRow()['tgr_state'] ?? null;
133    }
134
135    public function getWorkflowStatesForLanguage( string $languageCode, array $groupIds ): array {
136        $result = $this->getWorkflowStates( $groupIds, [ $languageCode ] );
137        $states = $this->result2map( $result, 'tgr_group', 'tgr_state' );
138
139        $finalResult = [];
140        foreach ( $groupIds as $groupId ) {
141            $dbGroupId = self::getGroupIdForDatabase( $groupId );
142            if ( isset( $states[ $dbGroupId ] ) ) {
143                $finalResult[ $groupId ] = $states[ $dbGroupId ];
144            }
145        }
146
147        return $finalResult;
148    }
149
150    public function getWorkflowStatesForGroup( string $groupId ): array {
151        $result = $this->getWorkflowStates( [ $groupId ], null );
152        return $this->result2map( $result, 'tgr_lang', 'tgr_state' );
153    }
154
155    private function getWorkflowStates( ?array $groupIds, ?array $languageCodes ): IResultWrapper {
156        $dbr = $this->loadBalancer->getConnection( DB_REPLICA );
157        $conditions = array_filter(
158            [ 'tgr_group' => $groupIds, 'tgr_lang' => $languageCodes ],
159            static fn ( $x ) => $x !== null && $x !== ''
160        );
161
162        if ( $conditions === [] ) {
163            throw new InvalidArgumentException( 'Either the $groupId or the $languageCode should be provided' );
164        }
165
166        if ( isset( $conditions['tgr_group'] ) ) {
167            $conditions['tgr_group'] = array_map( [ self::class, 'getGroupIdForDatabase' ], $groupIds );
168        }
169
170        return $dbr->newSelectQueryBuilder()
171            ->select( [ 'tgr_state', 'tgr_group', 'tgr_lang' ] )
172            ->from( self::TABLE_NAME )
173            ->where( $conditions )
174            ->caller( __METHOD__ )
175            ->fetchResultSet();
176    }
177
178    private function result2map( IResultWrapper $result, string $keyValue, string $valueValue ): array {
179        $map = [];
180        foreach ( $result as $row ) {
181            $map[$row->$keyValue] = $row->$valueValue;
182        }
183
184        return $map;
185    }
186
187    private static function getGroupIdForDatabase( $groupId ): string {
188        $groupId = strval( $groupId );
189
190        // Check if length is more than 200 bytes
191        if ( strlen( $groupId ) <= 200 ) {
192            return $groupId;
193        }
194
195        // We take 160 bytes of the original string and append the md5 hash (32 bytes)
196        return mb_strcut( $groupId, 0, 160 ) . '||' . hash( 'md5', $groupId );
197    }
198}