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 (Persistent) map of (action ID => string[]) */
18    private static $tagsToSet = [];
19
20    /**
21     * @var ChangeTagsManager
22     */
23    private $changeTagsManager;
24
25    public function __construct( ChangeTagsManager $changeTagsManager ) {
26        $this->changeTagsManager = $changeTagsManager;
27    }
28
29    /**
30     * Clear any buffered tag
31     */
32    public function clearBuffer(): void {
33        self::$tagsToSet = [];
34    }
35
36    public function addConditionsLimitTag( ActionSpecifier $specifier ): void {
37        $this->addTags( $specifier, [ $this->changeTagsManager->getCondsLimitTag() ] );
38    }
39
40    public function addTags( ActionSpecifier $specifier, array $tags ): void {
41        $id = $this->getActionID( $specifier );
42        $this->bufferTagsToSetByAction( [ $id => $tags ] );
43    }
44
45    /**
46     * @param string[][] $tagsByAction Map of (string => string[])
47     */
48    private function bufferTagsToSetByAction( array $tagsByAction ): void {
49        foreach ( $tagsByAction as $actionID => $tags ) {
50            self::$tagsToSet[ $actionID ] = array_unique(
51                array_merge( self::$tagsToSet[ $actionID ] ?? [], $tags )
52            );
53        }
54    }
55
56    /**
57     * @param string $id
58     * @param bool $clear
59     * @return array
60     */
61    private function getTagsForID( string $id, bool $clear = true ): array {
62        $val = self::$tagsToSet[$id] ?? [];
63        if ( $clear ) {
64            unset( self::$tagsToSet[$id] );
65        }
66        return $val;
67    }
68
69    /**
70     * @param RecentChange $recentChange
71     * @param bool $clear
72     * @return array
73     */
74    public function getTagsForRecentChange( RecentChange $recentChange, bool $clear = true ): array {
75        $id = $this->getIDFromRecentChange( $recentChange );
76        return $this->getTagsForID( $id, $clear );
77    }
78
79    private function getIDFromRecentChange( RecentChange $recentChange ): string {
80        $title = new TitleValue(
81            $recentChange->getAttribute( 'rc_namespace' ),
82            $recentChange->getAttribute( 'rc_title' )
83        );
84
85        $logType = $recentChange->getAttribute( 'rc_log_type' ) ?: 'edit';
86        if ( $logType === 'newusers' ) {
87            // XXX: as of 1.43, the following is never true
88            $action = $recentChange->getAttribute( 'rc_log_action' ) === 'autocreate' ?
89                'autocreateaccount' :
90                'createaccount';
91        } else {
92            $action = $logType;
93        }
94        $user = new UserIdentityValue(
95            $recentChange->getAttribute( 'rc_user' ),
96            $recentChange->getAttribute( 'rc_user_text' )
97        );
98        $specifier = new ActionSpecifier(
99            $action,
100            $title,
101            $user,
102            $recentChange->getAttribute( 'rc_ip' ) ?? '',
103            $user->getName()
104        );
105        return $this->getActionID( $specifier );
106    }
107
108    /**
109     * Get a unique identifier for the given action
110     *
111     * @param ActionSpecifier $specifier
112     * @return string
113     */
114    private function getActionID( ActionSpecifier $specifier ): string {
115        $username = $specifier->getUser()->getName();
116        $title = $specifier->getTitle();
117        if ( str_contains( $specifier->getAction(), 'createaccount' ) ) {
118            // TODO Move this to ActionSpecifier?
119            $username = $specifier->getAccountName();
120            '@phan-var string $username';
121            $title = new TitleValue( NS_USER, $username );
122        }
123
124        // Use a character that's not allowed in titles and usernames
125        $glue = '|';
126        return implode(
127            $glue,
128            [
129                $title->getNamespace() . ':' . $title->getText(),
130                $username,
131                $specifier->getAction()
132            ]
133        );
134    }
135}