Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
RecentChangesTranslationFilterHookHandler.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate;
5
6use ChangesListStringOptionsFilterGroup;
7use MediaWiki\Config\Config;
8use MediaWiki\Context\IContextSource;
9use MediaWiki\Context\RequestContext;
10use MediaWiki\Hook\SpecialRecentChangesPanelHook;
11use MediaWiki\Html\Html;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageQueryHook;
14use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageStructuredFiltersHook;
15use MediaWiki\Specials\SpecialRecentChanges;
16use MediaWiki\Storage\NameTableAccessException;
17use MediaWiki\Xml\XmlSelect;
18use RecentChange;
19use Wikimedia\Rdbms\ILoadBalancer;
20use Wikimedia\Rdbms\IReadableDatabase;
21
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
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
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 = Html::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
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 // Hard-coded string, as ChangeTags::CHANGE_TAG is a private const.
220 $tables['translatetags'] = 'change_tag';
221 $join_conds['translatetags'] = [
222 'LEFT JOIN',
223 [ 'translatetags.ct_rc_id=rc_id', 'translatetags.ct_tag_id' => $renderedPage ]
224 ];
225 $conds['translatetags.ct_tag_id'] = null;
226 } catch ( NameTableAccessException ) {
227 // Tag name does not yet exist in DB.
228 }
229 }
230
231 $namespaces = $this->getTranslateNamespaces();
232 $inNamespaceCond = 'rc_namespace IN (' .
233 $dbr->makeList( $namespaces ) . ')';
234 $notInNamespaceCond = 'rc_namespace NOT IN (' .
235 $dbr->makeList( $namespaces ) . ')';
236
237 $onlyCond = $dbr->makeList( [
238 $inNamespaceCond,
239 'rc_title ' .
240 $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() )
241 ], LIST_AND );
242 $siteCond = $dbr->makeList( [
243 $inNamespaceCond,
244 'rc_title NOT' .
245 $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() )
246 ], LIST_AND );
247
248 if ( count( $selectedValues ) === 3 ) {
249 // no filters
250 return;
251 }
252
253 if ( $selectedValues === [ 'filter', 'only' ] ) {
254 $conds[] = $dbr->makeList( [
255 $notInNamespaceCond,
256 $onlyCond
257 ], LIST_OR );
258 return;
259 }
260
261 if ( $selectedValues === [ 'filter', 'site' ] ) {
262 $conds[] = $dbr->makeList( [
263 $notInNamespaceCond,
264 $siteCond
265 ], LIST_OR );
266 return;
267 }
268
269 if ( $selectedValues === [ 'only', 'site' ] ) {
270 $conds[] = $inNamespaceCond;
271 return;
272 }
273
274 if ( $selectedValues === [ 'filter' ] ) {
275 $conds[] = $notInNamespaceCond;
276 return;
277 }
278
279 if ( $selectedValues === [ 'only' ] ) {
280 $conds[] = $onlyCond;
281 return;
282 }
283
284 if ( $selectedValues === [ 'site' ] ) {
285 $conds[] = $siteCond;
286 }
287 }
288 ]
289 );
290
291 $special->registerFilterGroup( $translationsGroup );
292 }
293}
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.