Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.16% covered (warning)
81.16%
56 / 69
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockMetricsHooks
81.16% covered (warning)
81.16%
56 / 69
33.33% covered (danger)
33.33%
1 / 3
17.71
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 onPermissionErrorAudit
81.82% covered (warning)
81.82%
54 / 66
0.00% covered (danger)
0.00%
0 / 1
15.18
 submitEvent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace WikimediaEvents\BlockMetrics;
4
5use MediaWiki\Block\Block;
6use MediaWiki\Extension\EventBus\EventFactory;
7use MediaWiki\Extension\EventLogging\EventLogging;
8use MediaWiki\Linker\LinkTarget;
9use MediaWiki\Logger\LoggerFactory;
10use MediaWiki\MediaWikiServices;
11use MediaWiki\Permissions\Hook\PermissionErrorAuditHook;
12use MediaWiki\Permissions\PermissionManager;
13use MediaWiki\User\UserFactory;
14use MediaWiki\User\UserIdentity;
15use Message;
16use RequestContext;
17
18/**
19 * Hooks related to T303995.
20 */
21class BlockMetricsHooks implements PermissionErrorAuditHook {
22
23    public const SCHEMA = '/analytics/mediawiki/accountcreation/block/4.0.0';
24
25    /** @var UserFactory */
26    private $userFactory;
27
28    /** @var EventFactory */
29    private $eventFactory;
30
31    /**
32     * @param UserFactory $userFactory
33     * @param EventFactory $eventFactory
34     */
35    public function __construct(
36        UserFactory $userFactory,
37        EventFactory $eventFactory
38    ) {
39        $this->userFactory = $userFactory;
40        $this->eventFactory = $eventFactory;
41    }
42
43    /** @inheritDoc */
44    public function onPermissionErrorAudit(
45        LinkTarget $title,
46        UserIdentity $user,
47        string $action,
48        string $rigor,
49        array $errors
50    ): void {
51        // Ignore RIGOR_QUICK checks for performance; those won't check blocks anyway.
52        if ( $action !== 'createaccount' || $rigor === PermissionManager::RIGOR_QUICK ) {
53            return;
54        }
55        // Possible block error keys from Block\BlockErrorFormatter::getBlockErrorMessageKey()
56        $blockedErrorKeys = [
57            'blockedtext',
58            'autoblockedtext',
59            'blockedtext-partial',
60            'systemblockedtext',
61            'blockedtext-composite'
62        ];
63        // Possible block error keys from GlobalBlocking extension GlobalBlocking::getUserBlockDetails()
64        $globalBlockedErrorKeys = [
65            'globalblocking-ipblocked',
66            'globalblocking-ipblocked-range',
67            'globalblocking-ipblocked-xff',
68            // WikimediaMessages versions
69            'wikimedia-globalblocking-ipblocked',
70            'wikimedia-globalblocking-ipblocked-range',
71            'wikimedia-globalblocking-ipblocked-xff',
72        ];
73        $isApi = defined( 'MW_API' ) || defined( 'MW_REST_API' );
74
75        $blockedErrorMsgs = $globalBlockedErrorMsgs = [];
76        foreach ( $errors as $error ) {
77            $errorMsg = Message::newFromSpecifier( $error );
78            $errorKey = $errorMsg->getKey();
79            if ( in_array( $errorKey, $blockedErrorKeys, true ) ) {
80                $blockedErrorMsgs[] = $errorMsg;
81            } elseif ( in_array( $errorKey, $globalBlockedErrorKeys, true ) ) {
82                $globalBlockedErrorMsgs[] = $errorMsg;
83            }
84        }
85        $allErrorMsgs = array_merge( $blockedErrorMsgs, $globalBlockedErrorMsgs );
86
87        if ( !$allErrorMsgs ) {
88            return;
89        }
90
91        $user = $this->userFactory->newFromUserIdentity( $user );
92
93        $block = null;
94        // Prefer the local block over the global one if both are set. This is somewhat arbitrary.
95        if ( $blockedErrorMsgs ) {
96            $block = MediaWikiServices::getInstance()->getBlockManager()
97                ->getCreateAccountBlock( $user, RequestContext::getMain()->getRequest(), true );
98        } elseif ( $globalBlockedErrorMsgs ) {
99            $block = $user->getGlobalBlock();
100        }
101
102        if ( $block ) {
103            $context = RequestContext::getMain();
104            foreach ( $allErrorMsgs as $msg ) {
105                $msg->setContext( $context )->useDatabase( false )->inLanguage( 'en' );
106            }
107            $rawExpiry = $block->getExpiry();
108            if ( wfIsInfinity( $rawExpiry ) ) {
109                $expiry = 'infinity';
110            } else {
111                $expiry = wfTimestamp( TS_ISO_8601, $rawExpiry );
112            }
113            $event = [
114                '$schema' => self::SCHEMA,
115                'block_id' => json_encode( $block->getIdentifier() ),
116                // @phan-suppress-next-line PhanTypeMismatchDimFetchNullable
117                'block_type' => Block::BLOCK_TYPES[ $block->getType() ] ?? 'other',
118                'block_expiry' => $expiry,
119                'block_scope' => $blockedErrorMsgs ? 'local' : 'global',
120                'error_message_keys' => array_map( static function ( Message $msg ) {
121                    return $msg->getKey();
122                }, $allErrorMsgs ),
123                'error_messages' => array_map( static function ( Message $msg ) {
124                    return $msg->plain();
125                }, $allErrorMsgs ),
126                'user_ip' => $user->getRequest()->getIP(),
127                'is_api' => $isApi,
128            ];
129            $event += $this->eventFactory->createMediaWikiCommonAttrs( $user );
130            $this->submitEvent( 'mediawiki.accountcreation_block', $event );
131        } else {
132            LoggerFactory::getInstance( 'WikimediaEvents' )->warning( 'Could not find block', [
133                'errorKeys' => implode( ',', array_map( static function ( Message $msg ) {
134                    return $msg->getKey();
135                }, $allErrorMsgs ) ),
136            ] );
137        }
138    }
139
140    /**
141     * PHPUnit test helper that allows mocking out the EventLogging dependency.
142     * @param string $streamName
143     * @param array $event
144     * @return void
145     */
146    protected function submitEvent( string $streamName, array $event ): void {
147        EventLogging::submit( $streamName, $event );
148    }
149
150}