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