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