Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
RecentChangesTranslationFilterHookHandler.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate;
5
6use MediaWiki\Config\Config;
7use MediaWiki\Context\IContextSource;
8use MediaWiki\Context\RequestContext;
9use MediaWiki\Html\Html;
10use MediaWiki\MediaWikiServices;
11use MediaWiki\RecentChanges\ChangesListStringOptionsFilterGroup;
12use MediaWiki\RecentChanges\RecentChange;
13use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageQueryHook;
14use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageStructuredFiltersHook;
15use MediaWiki\Specials\Hook\SpecialRecentChangesPanelHook;
16use MediaWiki\Specials\SpecialRecentChanges;
17use MediaWiki\Storage\NameTableAccessException;
18use MediaWiki\Xml\XmlSelect;
19use Wikimedia\Rdbms\IConnectionProvider;
20use Wikimedia\Rdbms\IReadableDatabase;
21
31 SpecialRecentChangesPanelHook,
32 ChangesListSpecialPageStructuredFiltersHook,
33 ChangesListSpecialPageQueryHook
34{
35
36 public function __construct(
37 private readonly IConnectionProvider $dbProvider,
38 private readonly Config $config,
39 ) {
40 }
41
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->dbProvider->getReplicaDatabase();
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
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
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 ) {
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}
Class to add a new filter to Special:RecentChanges which makes it possible to filter translations awa...
onChangesListSpecialPageQuery( $pageName, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
@inheritDoc
onSpecialRecentChangesPanel(&$extraOpts, $opts)
Adds a HTMl selector into $items @inheritDoc.
onChangesListSpecialPageStructuredFilters( $special)
Adds translations filters to structured UI @inheritDoc.