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 ChangeTags;
8use Config;
9use IContextSource;
10use MediaWiki\Hook\SpecialRecentChangesPanelHook;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageQueryHook;
13use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageStructuredFiltersHook;
14use MediaWiki\Specials\SpecialRecentChanges;
15use MediaWiki\Storage\NameTableAccessException;
16use RecentChange;
17use RequestContext;
18use Wikimedia\Rdbms\ILoadBalancer;
19use Wikimedia\Rdbms\IReadableDatabase;
20use Xml;
21use 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 */
31class 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}