Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 173
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
RecentChangesTranslationFilterHookHandler
0.00% covered (danger)
0.00%
0 / 173
0.00% covered (danger)
0.00%
0 / 6
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onChangesListSpecialPageQuery
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 getTranslateNamespaces
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 onSpecialRecentChangesPanel
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 isStructuredFilterUiEnabled
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 onChangesListSpecialPageStructuredFilters
0.00% covered (danger)
0.00%
0 / 124
0.00% covered (danger)
0.00%
0 / 1
182
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
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 */
30class 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}