Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChangeTagsManager
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 8
210
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 purgeTagCache
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getTagsDefinedByActiveFilters
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTagsDefinedByFilters
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loadTagsFromDb
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 loadTags
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 getCacheKeyForStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCondsLimitTag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\ChangeTags;
4
5use MediaWiki\ChangeTags\ChangeTagsStore;
6use MediaWiki\Extension\AbuseFilter\CentralDBManager;
7use MediaWiki\Extension\AbuseFilter\CentralDBNotAvailableException;
8use Wikimedia\ObjectCache\WANObjectCache;
9use Wikimedia\Rdbms\Database;
10use Wikimedia\Rdbms\IReadableDatabase;
11use Wikimedia\Rdbms\LBFactory;
12
13/**
14 * Database wrapper class which aids registering and reserving change tags
15 * used by relevant abuse filters
16 * @todo Consider verbose constants instead of boolean?
17 */
18class ChangeTagsManager {
19
20    public const SERVICE_NAME = 'AbuseFilterChangeTagsManager';
21    private const CONDS_LIMIT_TAG = 'abusefilter-condition-limit';
22
23    public function __construct(
24        private readonly ChangeTagsStore $changeTagsStore,
25        private readonly LBFactory $lbFactory,
26        private readonly WANObjectCache $cache,
27        private readonly CentralDBManager $centralDBManager
28    ) {
29    }
30
31    /**
32     * Purge all cache related to tags, both within AbuseFilter and in core
33     */
34    public function purgeTagCache(): void {
35        // xxx: this doesn't purge all existing caches, see T266105
36        $this->changeTagsStore->purgeTagCacheAll();
37        $this->cache->delete( $this->getCacheKeyForStatus( true ) );
38        $this->cache->delete( $this->getCacheKeyForStatus( false ) );
39    }
40
41    /**
42     * Return tags used by any active (enabled) filter, both local and global.
43     * @return string[]
44     */
45    public function getTagsDefinedByActiveFilters(): array {
46        return $this->loadTags( true );
47    }
48
49    /**
50     * Return tags used by any filter that is not deleted, both local and global.
51     * @return string[]
52     */
53    public function getTagsDefinedByFilters(): array {
54        return $this->loadTags( false );
55    }
56
57    /**
58     * @param IReadableDatabase $dbr
59     * @param bool $enabled
60     * @param bool $global
61     * @return string[]
62     */
63    private function loadTagsFromDb( IReadableDatabase $dbr, bool $enabled, bool $global = false ): array {
64        // This is a pretty awful hack.
65        $where = [
66            'afa_consequence' => 'tag',
67            'af_deleted' => 0
68        ];
69        if ( $enabled ) {
70            $where['af_enabled'] = 1;
71        }
72        if ( $global ) {
73            $where['af_global'] = 1;
74        }
75
76        $res = $dbr->newSelectQueryBuilder()
77            ->select( 'afa_parameters' )
78            ->from( 'abuse_filter_action' )
79            ->join( 'abuse_filter', null, 'afa_filter=af_id' )
80            ->where( $where )
81            ->caller( __METHOD__ )
82            ->fetchResultSet();
83
84        $tags = [];
85        foreach ( $res as $row ) {
86            $tags = array_merge(
87                $row->afa_parameters !== '' ? explode( "\n", $row->afa_parameters ) : [],
88                $tags
89            );
90        }
91        return $tags;
92    }
93
94    /**
95     * @param bool $enabled
96     * @return string[]
97     */
98    private function loadTags( bool $enabled ): array {
99        return $this->cache->getWithSetCallback(
100            $this->getCacheKeyForStatus( $enabled ),
101            WANObjectCache::TTL_MINUTE,
102            function ( $oldValue, &$ttl, array &$setOpts ) use ( $enabled ) {
103                $dbr = $this->lbFactory->getReplicaDatabase();
104                try {
105                    $globalDbr = $this->centralDBManager->getConnection( DB_REPLICA );
106                } catch ( CentralDBNotAvailableException ) {
107                    $globalDbr = null;
108                }
109
110                if ( $globalDbr !== null ) {
111                    // Account for any snapshot/replica DB lag
112                    $setOpts += Database::getCacheSetOptions( $dbr, $globalDbr );
113                    $tags = array_merge(
114                        $this->loadTagsFromDb( $dbr, $enabled ),
115                        $this->loadTagsFromDb( $globalDbr, $enabled, true )
116                    );
117                } else {
118                    $setOpts += Database::getCacheSetOptions( $dbr );
119                    $tags = $this->loadTagsFromDb( $dbr, $enabled );
120                }
121
122                return array_unique( $tags );
123            }
124        );
125    }
126
127    private function getCacheKeyForStatus( bool $enabled ): string {
128        return $this->cache->makeKey( 'abusefilter-fetch-all-tags', (int)$enabled );
129    }
130
131    /**
132     * Get the tag identifier used to indicate a change has exceeded the condition limit
133     */
134    public function getCondsLimitTag(): string {
135        return self::CONDS_LIMIT_TAG;
136    }
137}