Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 53
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 / 53
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 / 4
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 / 23
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 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    private ChangeTagsStore $changeTagsStore;
24    private LBFactory $lbFactory;
25    private WANObjectCache $cache;
26    private CentralDBManager $centralDBManager;
27
28    /**
29     * @param ChangeTagsStore $changeTagsStore
30     * @param LBFactory $lbFactory
31     * @param WANObjectCache $cache
32     * @param CentralDBManager $centralDBManager
33     */
34    public function __construct(
35        ChangeTagsStore $changeTagsStore,
36        LBFactory $lbFactory,
37        WANObjectCache $cache,
38        CentralDBManager $centralDBManager
39    ) {
40        $this->changeTagsStore = $changeTagsStore;
41        $this->lbFactory = $lbFactory;
42        $this->cache = $cache;
43        $this->centralDBManager = $centralDBManager;
44    }
45
46    /**
47     * Purge all cache related to tags, both within AbuseFilter and in core
48     */
49    public function purgeTagCache(): void {
50        // xxx: this doesn't purge all existing caches, see T266105
51        $this->changeTagsStore->purgeTagCacheAll();
52        $this->cache->delete( $this->getCacheKeyForStatus( true ) );
53        $this->cache->delete( $this->getCacheKeyForStatus( false ) );
54    }
55
56    /**
57     * Return tags used by any active (enabled) filter, both local and global.
58     * @return string[]
59     */
60    public function getTagsDefinedByActiveFilters(): array {
61        return $this->loadTags( true );
62    }
63
64    /**
65     * Return tags used by any filter that is not deleted, both local and global.
66     * @return string[]
67     */
68    public function getTagsDefinedByFilters(): array {
69        return $this->loadTags( false );
70    }
71
72    /**
73     * @param IReadableDatabase $dbr
74     * @param bool $enabled
75     * @param bool $global
76     * @return string[]
77     */
78    private function loadTagsFromDb( IReadableDatabase $dbr, bool $enabled, bool $global = false ): array {
79        // This is a pretty awful hack.
80        $where = [
81            'afa_consequence' => 'tag',
82            'af_deleted' => 0
83        ];
84        if ( $enabled ) {
85            $where['af_enabled'] = 1;
86        }
87        if ( $global ) {
88            $where['af_global'] = 1;
89        }
90
91        $res = $dbr->select(
92            [ 'abuse_filter_action', 'abuse_filter' ],
93            'afa_parameters',
94            $where,
95            __METHOD__,
96            [],
97            [ 'abuse_filter' => [ 'INNER JOIN', 'afa_filter=af_id' ] ]
98        );
99
100        $tags = [];
101        foreach ( $res as $row ) {
102            $tags = array_merge(
103                $row->afa_parameters !== '' ? explode( "\n", $row->afa_parameters ) : [],
104                $tags
105            );
106        }
107        return $tags;
108    }
109
110    /**
111     * @param bool $enabled
112     * @return string[]
113     */
114    private function loadTags( bool $enabled ): array {
115        return $this->cache->getWithSetCallback(
116            $this->getCacheKeyForStatus( $enabled ),
117            WANObjectCache::TTL_MINUTE,
118            function ( $oldValue, &$ttl, array &$setOpts ) use ( $enabled ) {
119                $dbr = $this->lbFactory->getReplicaDatabase();
120                try {
121                    $globalDbr = $this->centralDBManager->getConnection( DB_REPLICA );
122                } catch ( CentralDBNotAvailableException $_ ) {
123                    $globalDbr = null;
124                }
125
126                if ( $globalDbr !== null ) {
127                    // Account for any snapshot/replica DB lag
128                    $setOpts += Database::getCacheSetOptions( $dbr, $globalDbr );
129                    $tags = array_merge(
130                        $this->loadTagsFromDb( $dbr, $enabled ),
131                        $this->loadTagsFromDb( $globalDbr, $enabled, true )
132                    );
133                } else {
134                    $setOpts += Database::getCacheSetOptions( $dbr );
135                    $tags = $this->loadTagsFromDb( $dbr, $enabled );
136                }
137
138                return array_unique( $tags );
139            }
140        );
141    }
142
143    /**
144     * @param bool $enabled
145     * @return string
146     */
147    private function getCacheKeyForStatus( bool $enabled ): string {
148        return $this->cache->makeKey( 'abusefilter-fetch-all-tags', (int)$enabled );
149    }
150
151    /**
152     * Get the tag identifier used to indicate a change has exceeded the condition limit
153     * @return string
154     */
155    public function getCondsLimitTag(): string {
156        return self::CONDS_LIMIT_TAG;
157    }
158}