Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 173 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
RecentChangesTranslationFilterHookHandler | |
0.00% |
0 / 173 |
|
0.00% |
0 / 6 |
650 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onChangesListSpecialPageQuery | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
getTranslateNamespaces | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
onSpecialRecentChangesPanel | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
6 | |||
isStructuredFilterUiEnabled | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
onChangesListSpecialPageStructuredFilters | |
0.00% |
0 / 124 |
|
0.00% |
0 / 1 |
182 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate; |
5 | |
6 | use ChangesListStringOptionsFilterGroup; |
7 | use ChangeTags; |
8 | use Config; |
9 | use IContextSource; |
10 | use MediaWiki\Hook\SpecialRecentChangesPanelHook; |
11 | use MediaWiki\MediaWikiServices; |
12 | use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageQueryHook; |
13 | use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageStructuredFiltersHook; |
14 | use MediaWiki\Specials\SpecialRecentChanges; |
15 | use MediaWiki\Storage\NameTableAccessException; |
16 | use RecentChange; |
17 | use RequestContext; |
18 | use Wikimedia\Rdbms\ILoadBalancer; |
19 | use Wikimedia\Rdbms\IReadableDatabase; |
20 | use Xml; |
21 | use XmlSelect; |
22 | |
23 | /** |
24 | * Class to add a new filter to Special:RecentChanges which makes it possible to filter |
25 | * translations away or show them only. |
26 | * |
27 | * @author Niklas Laxström |
28 | * @copyright Copyright © 2010, Niklas Laxström |
29 | * @license GPL-2.0-or-later |
30 | */ |
31 | class RecentChangesTranslationFilterHookHandler implements |
32 | SpecialRecentChangesPanelHook, |
33 | ChangesListSpecialPageStructuredFiltersHook, |
34 | ChangesListSpecialPageQueryHook |
35 | { |
36 | private ILoadBalancer $loadBalancer; |
37 | private Config $config; |
38 | |
39 | public function __construct( ILoadBalancer $loadBalancer, Config $config ) { |
40 | $this->loadBalancer = $loadBalancer; |
41 | $this->config = $config; |
42 | } |
43 | |
44 | public function onChangesListSpecialPageQuery( |
45 | $pageName, |
46 | &$tables, |
47 | &$fields, |
48 | &$conds, |
49 | &$query_options, |
50 | &$join_conds, |
51 | $opts |
52 | ): void { |
53 | $translateRcFilterDefault = $this->config->get( 'TranslateRcFilterDefault' ); |
54 | |
55 | if ( $pageName !== 'Recentchanges' || $this->isStructuredFilterUiEnabled() ) { |
56 | return; |
57 | } |
58 | |
59 | $request = RequestContext::getMain()->getRequest(); |
60 | $translations = $request->getVal( 'translations', $translateRcFilterDefault ); |
61 | $opts->add( 'translations', $translateRcFilterDefault ); |
62 | $opts->setValue( 'translations', $translations ); |
63 | |
64 | $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); |
65 | |
66 | $namespaces = $this->getTranslateNamespaces(); |
67 | |
68 | if ( $translations === 'only' ) { |
69 | $conds[] = 'rc_namespace IN (' . $dbr->makeList( $namespaces ) . ')'; |
70 | $conds[] = 'rc_title like \'%%/%%\''; |
71 | } elseif ( $translations === 'filter' ) { |
72 | $conds[] = 'rc_namespace NOT IN (' . $dbr->makeList( $namespaces ) . ')'; |
73 | } elseif ( $translations === 'site' ) { |
74 | $conds[] = 'rc_namespace IN (' . $dbr->makeList( $namespaces ) . ')'; |
75 | $conds[] = 'rc_title not like \'%%/%%\''; |
76 | } |
77 | } |
78 | |
79 | private function getTranslateNamespaces(): array { |
80 | $translateMessageNamespaces = $this->config->get( 'TranslateMessageNamespaces' ); |
81 | $namespaces = []; |
82 | |
83 | foreach ( $translateMessageNamespaces as $index ) { |
84 | $namespaces[] = $index; |
85 | $namespaces[] = $index + 1; // Include Talk namespaces |
86 | } |
87 | |
88 | return $namespaces; |
89 | } |
90 | |
91 | /** |
92 | * Adds a HTMl selector into $items |
93 | * @inheritDoc |
94 | */ |
95 | public function onSpecialRecentChangesPanel( &$extraOpts, $opts ): void { |
96 | if ( $this->isStructuredFilterUiEnabled() ) { |
97 | return; |
98 | } |
99 | |
100 | $opts->consumeValue( 'translations' ); |
101 | $default = $opts->getValue( 'translations' ); |
102 | |
103 | $label = Xml::label( |
104 | wfMessage( 'translate-rc-translation-filter' )->text(), |
105 | 'mw-translation-filter' |
106 | ); |
107 | $select = new XmlSelect( 'translations', 'mw-translation-filter', $default ); |
108 | $select->addOption( |
109 | wfMessage( 'translate-rc-translation-filter-no' )->text(), |
110 | 'noaction' |
111 | ); |
112 | $select->addOption( wfMessage( 'translate-rc-translation-filter-only' )->text(), 'only' ); |
113 | $select->addOption( |
114 | wfMessage( 'translate-rc-translation-filter-filter' )->text(), |
115 | 'filter' |
116 | ); |
117 | $select->addOption( wfMessage( 'translate-rc-translation-filter-site' )->text(), 'site' ); |
118 | |
119 | $extraOpts['translations'] = [ $label, $select->getHTML() ]; |
120 | } |
121 | |
122 | private function isStructuredFilterUiEnabled(): bool { |
123 | $context = RequestContext::getMain(); |
124 | |
125 | // This assumes usage only on RC page |
126 | $page = new SpecialRecentChanges(); |
127 | $page->setContext( $context ); |
128 | |
129 | return $page->isStructuredFilterUiEnabled(); |
130 | } |
131 | |
132 | /** |
133 | * Adds translations filters to structured UI |
134 | * @inheritDoc |
135 | */ |
136 | public function onChangesListSpecialPageStructuredFilters( $special ): void { |
137 | $translateRcFilterDefault = $this->config->get( 'TranslateRcFilterDefault' ); |
138 | $defaultFilter = $translateRcFilterDefault !== 'noaction' ? |
139 | $translateRcFilterDefault : |
140 | ChangesListStringOptionsFilterGroup::NONE; |
141 | |
142 | $translationsGroup = new ChangesListStringOptionsFilterGroup( |
143 | [ |
144 | 'name' => 'translations', |
145 | 'title' => 'translate-rcfilters-translations', |
146 | 'priority' => -7, |
147 | 'default' => $defaultFilter, |
148 | 'isFullCoverage' => true, |
149 | 'filters' => [ |
150 | [ |
151 | 'name' => 'only', |
152 | 'label' => 'translate-rcfilters-translations-only-label', |
153 | 'description' => 'translate-rcfilters-translations-only-desc', |
154 | 'cssClassSuffix' => 'only', |
155 | 'isRowApplicableCallable' => function ( IContextSource $ctx, RecentChange $rc ) { |
156 | $namespaces = $this->getTranslateNamespaces(); |
157 | |
158 | return in_array( $rc->getAttribute( 'rc_namespace' ), $namespaces ) && |
159 | !str_contains( $rc->getAttribute( 'rc_title' ), '/' ); |
160 | } |
161 | ], |
162 | [ |
163 | 'name' => 'site', |
164 | 'label' => 'translate-rcfilters-translations-site-label', |
165 | 'description' => 'translate-rcfilters-translations-site-desc', |
166 | 'cssClassSuffix' => 'site', |
167 | 'isRowApplicableCallable' => function ( IContextSource $ctx, RecentChange $rc ) { |
168 | $namespaces = $this->getTranslateNamespaces(); |
169 | |
170 | return in_array( $rc->getAttribute( 'rc_namespace' ), $namespaces ) && |
171 | !str_contains( $rc->getAttribute( 'rc_title' ), '/' ); |
172 | } |
173 | ], |
174 | [ |
175 | 'name' => 'filter', |
176 | 'label' => 'translate-rcfilters-translations-filter-label', |
177 | 'description' => 'translate-rcfilters-translations-filter-desc', |
178 | 'cssClassSuffix' => 'filter', |
179 | 'isRowApplicableCallable' => function ( IContextSource $ctx, RecentChange $rc ) { |
180 | $namespaces = $this->getTranslateNamespaces(); |
181 | |
182 | return !in_array( $rc->getAttribute( 'rc_namespace' ), $namespaces ); |
183 | } |
184 | ], |
185 | [ |
186 | 'name' => 'filter-translation-pages', |
187 | 'label' => 'translate-rcfilters-translations-filter-translation-pages-label', |
188 | 'description' => 'translate-rcfilters-translations-filter-translation-pages-desc', |
189 | 'cssClassSuffix' => 'filter-translation-pages', |
190 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
191 | $tags = explode( ', ', $rc->getAttribute( 'ts_tags' ) ?? '' ); |
192 | return !in_array( 'translate-filter-translation-pages', $tags ); |
193 | } |
194 | ], |
195 | |
196 | ], |
197 | 'queryCallable' => function ( |
198 | string $specialClassName, |
199 | IContextSource $ctx, |
200 | IReadableDatabase $dbr, |
201 | array &$tables, |
202 | array &$fields, |
203 | array &$conds, |
204 | array &$query_options, |
205 | array &$join_conds, |
206 | array $selectedValues |
207 | ) { |
208 | $fields = array_merge( $fields, [ 'rc_title', 'rc_namespace' ] ); |
209 | |
210 | // Handle changes to translation pages separately |
211 | $filterRenderedIndex = array_search( 'filter-translation-pages', $selectedValues ); |
212 | if ( $filterRenderedIndex !== false ) { |
213 | unset( $selectedValues[$filterRenderedIndex] ); |
214 | $selectedValues = array_values( $selectedValues ); |
215 | |
216 | $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); |
217 | try { |
218 | $renderedPage = $changeTagDefStore->getId( 'translate-translation-pages' ); |
219 | // The recommended replacement is for this deprecared method is `ChangeTags::CHANGE_TAG`. |
220 | // However, ChangeTags::CHANGE_TAG is a private const. |
221 | $tables['translatetags'] = ChangeTags::getDisplayTableName(); |
222 | $join_conds['translatetags'] = [ |
223 | 'LEFT JOIN', |
224 | [ 'translatetags.ct_rc_id=rc_id', 'translatetags.ct_tag_id' => $renderedPage ] |
225 | ]; |
226 | $conds['translatetags.ct_tag_id'] = null; |
227 | } catch ( NameTableAccessException $exception ) { |
228 | // Tag name does not yet exist in DB. |
229 | } |
230 | } |
231 | |
232 | $namespaces = $this->getTranslateNamespaces(); |
233 | $inNamespaceCond = 'rc_namespace IN (' . |
234 | $dbr->makeList( $namespaces ) . ')'; |
235 | $notInNamespaceCond = 'rc_namespace NOT IN (' . |
236 | $dbr->makeList( $namespaces ) . ')'; |
237 | |
238 | $onlyCond = $dbr->makeList( [ |
239 | $inNamespaceCond, |
240 | 'rc_title ' . |
241 | $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ) |
242 | ], LIST_AND ); |
243 | $siteCond = $dbr->makeList( [ |
244 | $inNamespaceCond, |
245 | 'rc_title NOT' . |
246 | $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ) |
247 | ], LIST_AND ); |
248 | |
249 | if ( count( $selectedValues ) === 3 ) { |
250 | // no filters |
251 | return; |
252 | } |
253 | |
254 | if ( $selectedValues === [ 'filter', 'only' ] ) { |
255 | $conds[] = $dbr->makeList( [ |
256 | $notInNamespaceCond, |
257 | $onlyCond |
258 | ], LIST_OR ); |
259 | return; |
260 | } |
261 | |
262 | if ( $selectedValues === [ 'filter', 'site' ] ) { |
263 | $conds[] = $dbr->makeList( [ |
264 | $notInNamespaceCond, |
265 | $siteCond |
266 | ], LIST_OR ); |
267 | return; |
268 | } |
269 | |
270 | if ( $selectedValues === [ 'only', 'site' ] ) { |
271 | $conds[] = $inNamespaceCond; |
272 | return; |
273 | } |
274 | |
275 | if ( $selectedValues === [ 'filter' ] ) { |
276 | $conds[] = $notInNamespaceCond; |
277 | return; |
278 | } |
279 | |
280 | if ( $selectedValues === [ 'only' ] ) { |
281 | $conds[] = $onlyCond; |
282 | return; |
283 | } |
284 | |
285 | if ( $selectedValues === [ 'site' ] ) { |
286 | $conds[] = $siteCond; |
287 | } |
288 | } |
289 | ] |
290 | ); |
291 | |
292 | $special->registerFilterGroup( $translationsGroup ); |
293 | } |
294 | } |