Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
ChangeTagger
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
9 / 9
15
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
 clearBuffer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addConditionsLimitTag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addTags
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 bufferTagsToSetByAction
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getTagsForID
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getTagsForRecentChange
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getIDFromRecentChange
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
4
 getActionID
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\ChangeTags;
4
5use MediaWiki\Extension\AbuseFilter\ActionSpecifier;
6use MediaWiki\RecentChanges\RecentChange;
7use MediaWiki\Title\TitleValue;
8use MediaWiki\User\UserIdentityValue;
9
10/**
11 * Class that collects change tags to be later applied
12 * @internal This interface should be improved and is not ready for external use
13 */
14class ChangeTagger {
15    public const SERVICE_NAME = 'AbuseFilterChangeTagger';
16
17    /** @var array<string,string[]> (Persistent) map of (action ID => string[]) */
18    private static $tagsToSet = [];
19
20    public function __construct( private readonly ChangeTagsManager $changeTagsManager ) {
21    }
22
23    /**
24     * Clear any buffered tag
25     */
26    public function clearBuffer(): void {
27        self::$tagsToSet = [];
28    }
29
30    public function addConditionsLimitTag( ActionSpecifier $specifier ): void {
31        $this->addTags( $specifier, [ $this->changeTagsManager->getCondsLimitTag() ] );
32    }
33
34    public function addTags( ActionSpecifier $specifier, array $tags ): void {
35        $id = $this->getActionID( $specifier );
36        $this->bufferTagsToSetByAction( [ $id => $tags ] );
37    }
38
39    /**
40     * @param string[][] $tagsByAction Map of (string => string[])
41     */
42    private function bufferTagsToSetByAction( array $tagsByAction ): void {
43        foreach ( $tagsByAction as $actionID => $tags ) {
44            self::$tagsToSet[ $actionID ] = array_unique(
45                array_merge( self::$tagsToSet[ $actionID ] ?? [], $tags )
46            );
47        }
48    }
49
50    /**
51     * @param string $id
52     * @param bool $clear
53     * @return array
54     */
55    private function getTagsForID( string $id, bool $clear = true ): array {
56        $val = self::$tagsToSet[$id] ?? [];
57        if ( $clear ) {
58            unset( self::$tagsToSet[$id] );
59        }
60        return $val;
61    }
62
63    /**
64     * @param RecentChange $recentChange
65     * @param bool $clear
66     * @return array
67     */
68    public function getTagsForRecentChange( RecentChange $recentChange, bool $clear = true ): array {
69        $id = $this->getIDFromRecentChange( $recentChange );
70        return $this->getTagsForID( $id, $clear );
71    }
72
73    private function getIDFromRecentChange( RecentChange $recentChange ): string {
74        $title = new TitleValue(
75            $recentChange->getAttribute( 'rc_namespace' ),
76            $recentChange->getAttribute( 'rc_title' )
77        );
78
79        $logType = $recentChange->getAttribute( 'rc_log_type' ) ?: 'edit';
80        if ( $logType === 'newusers' ) {
81            // XXX: as of 1.43, the following is never true
82            $action = $recentChange->getAttribute( 'rc_log_action' ) === 'autocreate' ?
83                'autocreateaccount' :
84                'createaccount';
85        } else {
86            $action = $logType;
87        }
88        $user = new UserIdentityValue(
89            $recentChange->getAttribute( 'rc_user' ),
90            $recentChange->getAttribute( 'rc_user_text' )
91        );
92        $specifier = new ActionSpecifier(
93            $action,
94            $title,
95            $user,
96            $recentChange->getAttribute( 'rc_ip' ) ?? '',
97            $user->getName()
98        );
99        return $this->getActionID( $specifier );
100    }
101
102    /**
103     * Get a unique identifier for the given action
104     *
105     * @param ActionSpecifier $specifier
106     * @return string
107     */
108    private function getActionID( ActionSpecifier $specifier ): string {
109        $username = $specifier->getUser()->getName();
110        $title = $specifier->getTitle();
111        if ( str_contains( $specifier->getAction(), 'createaccount' ) ) {
112            // TODO Move this to ActionSpecifier?
113            $username = $specifier->getAccountName();
114            '@phan-var string $username';
115            $title = new TitleValue( NS_USER, $username );
116        }
117
118        // Use a character that's not allowed in titles and usernames
119        $glue = '|';
120        return implode(
121            $glue,
122            [
123                $title->getNamespace() . ':' . $title->getText(),
124                $username,
125                $specifier->getAction()
126            ]
127        );
128    }
129}