Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 118 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
RevTagStore | |
0.00% |
0 / 118 |
|
0.00% |
0 / 9 |
380 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
replaceTag | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
12 | |||
getLatestRevisionWithTag | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getLatestRevisionsForTags | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
42 | |||
removeTags | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
isRevIdFuzzy | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
getTransver | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
setTransver | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
getTranslatableBundleIds | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageGroupProcessing; |
5 | |
6 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
7 | use MediaWiki\Page\PageIdentity; |
8 | use Wikimedia\Rdbms\IConnectionProvider; |
9 | use Wikimedia\Rdbms\SelectQueryBuilder; |
10 | |
11 | /** |
12 | * Class to manage revision tags for translatable bundles. |
13 | * @author Abijeet Patro |
14 | * @author Niklas Laxström |
15 | * @since 2022.04 |
16 | * @license GPL-2.0-or-later |
17 | */ |
18 | class RevTagStore { |
19 | /** Indicates that a translation is fuzzy (outdated or not passing validation). */ |
20 | public const FUZZY_TAG = 'fuzzy'; |
21 | /** Stores the revision id of the corresponding source text. Used for showing diffs for outdated messages. */ |
22 | public const TRANSVER_PROP = 'tp:transver'; |
23 | /** Indicates a revision of a page that can be marked for translation. */ |
24 | public const TP_MARK_TAG = 'tp:mark'; |
25 | /** Indicates a revision of a translatable page that is marked for translation. */ |
26 | public const TP_READY_TAG = 'tp:tag'; |
27 | /** Indicates a revision of a page that is a valid message bundle. */ |
28 | public const MB_VALID_TAG = 'mb:valid'; |
29 | |
30 | private IConnectionProvider $dbProvider; |
31 | private array $tagCache = []; |
32 | |
33 | public function __construct( IConnectionProvider $dbProvider ) { |
34 | $this->dbProvider = $dbProvider; |
35 | } |
36 | |
37 | /** Add tag for the given revisionId, while deleting it from others */ |
38 | public function replaceTag( |
39 | PageIdentity $identity, |
40 | string $tag, |
41 | int $revisionId, |
42 | ?array $value = null |
43 | ): void { |
44 | if ( !$identity->exists() ) { |
45 | return; |
46 | } |
47 | |
48 | $articleId = $identity->getId(); |
49 | |
50 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
51 | $conditions = [ |
52 | 'rt_page' => $articleId, |
53 | 'rt_type' => $tag |
54 | ]; |
55 | $dbw->newDeleteQueryBuilder() |
56 | ->deleteFrom( 'revtag' ) |
57 | ->where( $conditions ) |
58 | ->caller( __METHOD__ ) |
59 | ->execute(); |
60 | |
61 | if ( $value !== null ) { |
62 | $conditions['rt_value'] = serialize( implode( '|', $value ) ); |
63 | } |
64 | |
65 | $conditions['rt_revision'] = $revisionId; |
66 | $dbw->newInsertQueryBuilder() |
67 | ->insertInto( 'revtag' ) |
68 | ->row( $conditions ) |
69 | ->caller( __METHOD__ ) |
70 | ->execute(); |
71 | |
72 | $this->tagCache[$articleId][$tag] = $revisionId; |
73 | } |
74 | |
75 | public function getLatestRevisionWithTag( PageIdentity $identity, string $tag ): ?int { |
76 | $response = $this->getLatestRevisionsForTags( $identity, $tag ); |
77 | return $response[$tag] ?? null; |
78 | } |
79 | |
80 | /** @return null|int[] */ |
81 | public function getLatestRevisionsForTags( PageIdentity $identity, string ...$tags ): ?array { |
82 | if ( !$identity->exists() ) { |
83 | return null; |
84 | } |
85 | |
86 | $articleId = $identity->getId(); |
87 | |
88 | $response = []; |
89 | $remainingTags = []; |
90 | |
91 | // ATTENTION: Cache should only be updated on POST requests. |
92 | foreach ( $tags as $tag ) { |
93 | if ( isset( $this->tagCache[$articleId][$tag] ) ) { |
94 | $response[$tag] = $this->tagCache[$articleId][$tag]; |
95 | } else { |
96 | $remainingTags[] = $tag; |
97 | } |
98 | } |
99 | |
100 | if ( !$remainingTags ) { |
101 | // All tags were available in the cache, no need to run any queries. |
102 | return $response; |
103 | } |
104 | |
105 | $dbr = Utilities::getSafeReadDB(); |
106 | $results = $dbr->newSelectQueryBuilder() |
107 | ->select( [ 'rt_revision' => 'MAX(rt_revision)', 'rt_type' ] ) |
108 | ->from( 'revtag' ) |
109 | ->where( [ |
110 | 'rt_page' => $articleId, |
111 | 'rt_type' => $remainingTags |
112 | ] ) |
113 | ->groupBy( 'rt_type' ) |
114 | ->caller( __METHOD__ ) |
115 | ->fetchResultSet(); |
116 | |
117 | foreach ( $results as $row ) { |
118 | $response[$row->rt_type] = (int)$row->rt_revision; |
119 | } |
120 | |
121 | return $response; |
122 | } |
123 | |
124 | public function removeTags( PageIdentity $identity, string ...$tag ): void { |
125 | if ( !$identity->exists() ) { |
126 | return; |
127 | } |
128 | |
129 | $articleId = $identity->getId(); |
130 | |
131 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
132 | $dbw->newDeleteQueryBuilder() |
133 | ->deleteFrom( 'revtag' ) |
134 | ->where( [ |
135 | 'rt_page' => $articleId, |
136 | 'rt_type' => $tag, |
137 | ] ) |
138 | ->caller( __METHOD__ ) |
139 | ->execute(); |
140 | |
141 | unset( $this->tagCache[$articleId] ); |
142 | } |
143 | |
144 | public function isRevIdFuzzy( int $articleId, int $revisionId ): bool { |
145 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
146 | $res = $dbw->newSelectQueryBuilder() |
147 | ->select( 'rt_type' ) |
148 | ->from( 'revtag' ) |
149 | ->where( [ |
150 | 'rt_page' => $articleId, |
151 | 'rt_type' => self::FUZZY_TAG, |
152 | 'rt_revision' => $revisionId |
153 | ] ) |
154 | ->caller( __METHOD__ ) |
155 | ->fetchField(); |
156 | |
157 | return $res !== false; |
158 | } |
159 | |
160 | /** |
161 | * Get the revision ID of the original message that was live the last |
162 | * time a non-fuzzy translation was saved (presumably the version whose |
163 | * translation the translation is). Used to determine whether the |
164 | * translation is outdated and to show a diff of the original message |
165 | * if it is. |
166 | * @return int|null The revision ID, or `null` if none is found |
167 | */ |
168 | public function getTransver( PageIdentity $identity ): ?int { |
169 | $db = Utilities::getSafeReadDB(); |
170 | $result = $db->newSelectQueryBuilder() |
171 | ->select( 'rt_value' ) |
172 | ->from( 'revtag' ) |
173 | ->where( [ |
174 | 'rt_page' => $identity->getId(), |
175 | 'rt_type' => self::TRANSVER_PROP, |
176 | ] ) |
177 | ->orderBy( 'rt_revision', SelectQueryBuilder::SORT_DESC ) |
178 | ->caller( __METHOD__ ) |
179 | ->fetchField(); |
180 | if ( $result === false ) { |
181 | return null; |
182 | } else { |
183 | // The revtag database is defined to store a string in rt_value, |
184 | // but tp:transver is always an integer |
185 | return (int)$result; |
186 | } |
187 | } |
188 | |
189 | /** |
190 | * Sets the tp:transver revtag of the given page/revision. This |
191 | * normally represents the last time a non-fuzzy translation was saved |
192 | * (presumably the version whose translation the translation is). |
193 | * and is used to determine whether the translation is outdated |
194 | * and to show a diff of the original message if it is. |
195 | * @return int|null The revision ID, or `null` if none is found |
196 | */ |
197 | public function setTransver( PageIdentity $identity, int $translationRevision, int $transver ) { |
198 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
199 | |
200 | $conds = [ |
201 | 'rt_page' => $identity->getId(), |
202 | 'rt_type' => self::TRANSVER_PROP, |
203 | 'rt_revision' => $translationRevision, |
204 | 'rt_value' => $transver, |
205 | ]; |
206 | $dbw->newReplaceQueryBuilder() |
207 | ->replaceInto( 'revtag' ) |
208 | ->uniqueIndexFields( [ 'rt_type', 'rt_page', 'rt_revision' ] ) |
209 | ->row( $conds ) |
210 | ->caller( __METHOD__ ) |
211 | ->execute(); |
212 | } |
213 | |
214 | /** Get a list of page ids where the latest revision is either tagged or marked */ |
215 | public static function getTranslatableBundleIds( string ...$revTags ): array { |
216 | $dbr = Utilities::getSafeReadDB(); |
217 | $res = $dbr->newSelectQueryBuilder() |
218 | ->select( 'rt_page' ) |
219 | ->from( 'revtag' ) |
220 | ->join( |
221 | 'page', |
222 | null, |
223 | [ 'rt_page = page_id', 'rt_revision = page_latest', 'rt_type' => $revTags ] |
224 | ) |
225 | ->groupBy( 'rt_page' ) |
226 | ->caller( __METHOD__ ) |
227 | ->fetchResultSet(); |
228 | $results = []; |
229 | foreach ( $res as $row ) { |
230 | $results[] = (int)$row->rt_page; |
231 | } |
232 | |
233 | return $results; |
234 | } |
235 | } |