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