Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 52
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 / 52
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 / 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    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->newSelectQueryBuilder()
92            ->select( 'afa_parameters' )
93            ->from( 'abuse_filter_action' )
94            ->join( 'abuse_filter', null, 'afa_filter=af_id' )
95            ->where( $where )
96            ->caller( __METHOD__ )
97            ->fetchResultSet();
98
99        $tags = [];
100        foreach ( $res as $row ) {
101            $tags = array_merge(
102                $row->afa_parameters !== '' ? explode( "\n", $row->afa_parameters ) : [],
103                $tags
104            );
105        }
106        return $tags;
107    }
108
109    /**
110     * @param bool $enabled
111     * @return string[]
112     */
113    private function loadTags( bool $enabled ): array {
114        return $this->cache->getWithSetCallback(
115            $this->getCacheKeyForStatus( $enabled ),
116            WANObjectCache::TTL_MINUTE,
117            function ( $oldValue, &$ttl, array &$setOpts ) use ( $enabled ) {
118                $dbr = $this->lbFactory->getReplicaDatabase();
119                try {
120                    $globalDbr = $this->centralDBManager->getConnection( DB_REPLICA );
121                } catch ( CentralDBNotAvailableException $_ ) {
122                    $globalDbr = null;
123                }
124
125                if ( $globalDbr !== null ) {
126                    // Account for any snapshot/replica DB lag
127                    $setOpts += Database::getCacheSetOptions( $dbr, $globalDbr );
128                    $tags = array_merge(
129                        $this->loadTagsFromDb( $dbr, $enabled ),
130                        $this->loadTagsFromDb( $globalDbr, $enabled, true )
131                    );
132                } else {
133                    $setOpts += Database::getCacheSetOptions( $dbr );
134                    $tags = $this->loadTagsFromDb( $dbr, $enabled );
135                }
136
137                return array_unique( $tags );
138            }
139        );
140    }
141
142    /**
143     * @param bool $enabled
144     * @return string
145     */
146    private function getCacheKeyForStatus( bool $enabled ): string {
147        return $this->cache->makeKey( 'abusefilter-fetch-all-tags', (int)$enabled );
148    }
149
150    /**
151     * Get the tag identifier used to indicate a change has exceeded the condition limit
152     * @return string
153     */
154    public function getCondsLimitTag(): string {
155        return self::CONDS_LIMIT_TAG;
156    }
157}