Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
AbuseFilterLogDetailsLookup
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
4 / 4
12
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIPForAbuseFilterLog
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 getIPsForAbuseFilterLogs
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 groupAbuseFilterLogIdsByPerformer
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter;
4
5use MediaWiki\Permissions\Authority;
6use StatusValue;
7use Wikimedia\IPUtils;
8use Wikimedia\Rdbms\IConnectionProvider;
9
10/**
11 * This service allows code to lookup details about abuse_filter_log rows.
12 *
13 * @since 1.45
14 */
15class AbuseFilterLogDetailsLookup {
16
17    public const SERVICE_NAME = 'AbuseFilterLogDetailsLookup';
18
19    public function __construct(
20        private readonly IConnectionProvider $dbProvider,
21        private readonly AbuseFilterPermissionManager $afPermissionManager,
22        private readonly FilterLookup $filterLookup
23    ) {
24    }
25
26    /**
27     * Returns the IP address associated with a given abuse_filter_log row identified by its afl_id.
28     *
29     * This method does NOT check whether the given {@link Authority} has the right to see IP
30     * addresses in general, but does check if the user can see each associated abuse_filter_log row.
31     *
32     * Callers are expected to log the access if the given {@link Authority} actually does view
33     * the IP address that was returned.
34     *
35     * @param Authority $authority The user who's trying to view the IP address.
36     * @param int $id The afl_id values for the rows
37     * @return StatusValue If a good status, the associated IP address is stored as the value of the status.
38     *    The IP address may be empty string if the row is more than $wgAbuseFilterLogIPMaxAge seconds old
39     *    or $wgAbuseFilterLogIP was false when the row was created.
40     */
41    public function getIPForAbuseFilterLog( Authority $authority, int $id ): StatusValue {
42        $row = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
43            ->select( [ 'afl_filter_id', 'afl_global', 'afl_ip_hex', 'afl_user_text' ] )
44            ->from( 'abuse_filter_log' )
45            ->where( [ 'afl_id' => $id ] )
46            ->caller( __METHOD__ )
47            ->fetchRow();
48
49        if ( !$row ) {
50            return StatusValue::newFatal( 'abusefilter-log-nonexistent' );
51        }
52
53        $filter = $this->filterLookup->getFilter( $row->afl_filter_id, $row->afl_global );
54        if ( !$this->afPermissionManager->canSeeIPForFilterLog( $authority, $filter, $row->afl_user_text ) ) {
55            return StatusValue::newFatal( 'abusefilter-log-cannot-see-details' );
56        }
57
58        return StatusValue::newGood( $row->afl_ip_hex ? IPUtils::formatHex( $row->afl_ip_hex ) : '' );
59    }
60
61    /**
62     * Returns the IP addresses associated with given abuse_filter_log rows identified by their afl_id.
63     *
64     * This method does NOT check whether the given {@link Authority} has the right to see IP
65     * addresses in general, except for temporary account IPs, but does check if the user can see
66     * each associated abuse_filter_log row.
67     *
68     * Callers are expected to log the access if the given {@link Authority} actually does view
69     * the IP address that was returned.
70     *
71     * If only ever looking up one IP, callers are advised to use
72     * {@link AbuseFilterLogDetailsLookup::getIPForAbuseFilterLog} instead.
73     *
74     * @stable to call
75     * @param Authority $authority The user who's trying to view the IP address.
76     * @param int[] $ids The afl_id values for the rows
77     * @return string[]|false[] The IPs found with indexed by the associated afl_id. The IP is false if the user cannot
78     *     see the IP. The IP is an empty string if the row is more than $wgAbuseFilterLogIPMaxAge seconds old,
79     *     no such afl_id exists, or $wgAbuseFilterLogIP was false when the row was created.
80     */
81    public function getIPsForAbuseFilterLogs( Authority $authority, array $ids ): array {
82        $rows = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
83            ->select( [ 'afl_filter_id', 'afl_global', 'afl_ip_hex', 'afl_id', 'afl_user_text' ] )
84            ->from( 'abuse_filter_log' )
85            ->where( [ 'afl_id' => $ids ] )
86            ->caller( __METHOD__ )
87            ->fetchResultSet();
88
89        $returnArray = array_fill_keys( $ids, '' );
90        foreach ( $rows as $row ) {
91            $filter = $this->filterLookup->getFilter( $row->afl_filter_id, $row->afl_global );
92            if ( !$this->afPermissionManager->canSeeIPForFilterLog( $authority, $filter, $row->afl_user_text ) ) {
93                $returnArray[$row->afl_id] = false;
94            } else {
95                $returnArray[$row->afl_id] = $row->afl_ip_hex ? IPUtils::formatHex( $row->afl_ip_hex ) : '';
96            }
97        }
98
99        return $returnArray;
100    }
101
102    /**
103     * Groups the provided list of afl_id values by the user who triggered the abuse_filter_log row to be created.
104     *
105     * @stable to call
106     * @param int[] $ids The afl_id values for the rows
107     * @return int[][] The afl_id values grouped by the user who triggered theabuse_filter_log row to be created.
108     */
109    public function groupAbuseFilterLogIdsByPerformer( array $ids ): array {
110        $rows = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
111            ->select( [ 'afl_user_text', 'afl_id' ] )
112            ->from( 'abuse_filter_log' )
113            ->where( [ 'afl_id' => $ids ] )
114            ->caller( __METHOD__ )
115            ->fetchResultSet();
116
117        $returnArray = [];
118        foreach ( $rows as $row ) {
119            if ( !array_key_exists( $row->afl_user_text, $returnArray ) ) {
120                $returnArray[$row->afl_user_text] = [];
121            }
122
123            $returnArray[$row->afl_user_text][] = $row->afl_id;
124        }
125
126        return $returnArray;
127    }
128}