Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
71 / 71
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
LogHandler
100.00% covered (success)
100.00%
71 / 71
100.00% covered (success)
100.00%
4 / 4
15
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 factory
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 getInfo
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
10
 isAnonymousOrTempUser
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace MediaWiki\IPInfo\Rest\Handler;
4
5use DatabaseLogEntry;
6use JobQueueGroup;
7use LogEventsList;
8use LogPage;
9use MediaWiki\IPInfo\InfoManager;
10use MediaWiki\IPInfo\Rest\Presenter\DefaultPresenter;
11use MediaWiki\IPInfo\TempUserIPLookup;
12use MediaWiki\Languages\LanguageFallback;
13use MediaWiki\Permissions\PermissionManager;
14use MediaWiki\Rest\LocalizedHttpException;
15use MediaWiki\User\Options\UserOptionsLookup;
16use MediaWiki\User\UserFactory;
17use MediaWiki\User\UserIdentity;
18use MediaWiki\User\UserIdentityLookup;
19use MediaWiki\User\UserIdentityUtils;
20use Wikimedia\IPUtils;
21use Wikimedia\Message\MessageValue;
22use Wikimedia\Rdbms\IConnectionProvider;
23
24class LogHandler extends IPInfoHandler {
25
26    private IConnectionProvider $dbProvider;
27
28    private UserIdentityLookup $userIdentityLookup;
29
30    public function __construct(
31        InfoManager $infoManager,
32        IConnectionProvider $dbProvider,
33        PermissionManager $permissionManager,
34        UserOptionsLookup $userOptionsLookup,
35        UserFactory $userFactory,
36        DefaultPresenter $presenter,
37        JobQueueGroup $jobQueueGroup,
38        LanguageFallback $languageFallback,
39        UserIdentityUtils $userIdentityUtils,
40        UserIdentityLookup $userIdentityLookup,
41        TempUserIPLookup $tempUserIPLookup
42    ) {
43        parent::__construct(
44            $infoManager,
45            $permissionManager,
46            $userOptionsLookup,
47            $userFactory,
48            $presenter,
49            $jobQueueGroup,
50            $languageFallback,
51            $userIdentityUtils,
52            $tempUserIPLookup
53        );
54        $this->dbProvider = $dbProvider;
55        $this->userIdentityLookup = $userIdentityLookup;
56    }
57
58    public static function factory(
59        InfoManager $infoManager,
60        IConnectionProvider $dbProvider,
61        PermissionManager $permissionManager,
62        UserOptionsLookup $userOptionsLookup,
63        UserFactory $userFactory,
64        JobQueueGroup $jobQueueGroup,
65        LanguageFallback $languageFallback,
66        UserIdentityUtils $userIdentityUtils,
67        UserIdentityLookup $userIdentityLookup,
68        TempUserIPLookup $tempUserIPLookup
69    ): self {
70        return new self(
71            $infoManager,
72            $dbProvider,
73            $permissionManager,
74            $userOptionsLookup,
75            $userFactory,
76            new DefaultPresenter( $permissionManager ),
77            $jobQueueGroup,
78            $languageFallback,
79            $userIdentityUtils,
80            $userIdentityLookup,
81            $tempUserIPLookup
82        );
83    }
84
85    /** @inheritDoc */
86    protected function getInfo( int $id ): array {
87        $dbr = $this->dbProvider->getReplicaDatabase();
88        $entry = DatabaseLogEntry::newFromId( $id, $dbr );
89
90        if ( !$entry ) {
91            throw new LocalizedHttpException(
92                new MessageValue( 'ipinfo-rest-log-nonexistent' ),
93                404
94            );
95        }
96
97        if ( !LogEventsList::userCanViewLogType( $entry->getType(), $this->getAuthority() ) ) {
98            throw new LocalizedHttpException(
99                new MessageValue( 'ipinfo-rest-log-denied' ),
100                403
101            );
102        }
103
104        // A log entry logs an action performed by a performer, on a target. Either the
105        // performer, or the target may be an IP address. This returns info about whichever is an
106        // IP address, or both, if both are IP addresses.
107        $canAccessPerformer = LogEventsList::userCanBitfield( $entry->getDeleted(), LogPage::DELETED_USER,
108            $this->getAuthority() );
109        $canAccessTarget = LogEventsList::userCanBitfield( $entry->getDeleted(), LogPage::DELETED_ACTION,
110            $this->getAuthority() );
111
112        // If the user cannot access the performer, nor the target, throw an error since there won't
113        // be anything to respond with.
114        if ( !$canAccessPerformer && !$canAccessTarget ) {
115            throw new LocalizedHttpException(
116                new MessageValue( 'ipinfo-rest-log-denied' ),
117                403
118            );
119        }
120
121        $performer = $entry->getPerformerIdentity();
122        $target = $this->userIdentityLookup->getUserIdentityByName( $entry->getTarget()->getText() );
123
124        $info = [];
125        $showPerformer = $canAccessPerformer && $this->isAnonymousOrTempUser( $performer );
126        $showTarget = $canAccessTarget && $this->isAnonymousOrTempUser( $target );
127        if ( $showPerformer ) {
128            $performerAddress = $this->tempUserIPLookup->getAddressForLogEntry( $entry );
129            $info[] = $this->presenter->present(
130                $this->infoManager->retrieveFor( $performer, $performerAddress ),
131                $this->getAuthority()->getUser()
132            );
133        }
134        if ( $showTarget ) {
135            // $target is implicitly null-checked via isAnonymousOrTempUser()
136            '@phan-var UserIdentity $target';
137
138            $targetAddress = $this->tempUserIPLookup->getMostRecentAddress( $target );
139
140            $info[] = $this->presenter->present( $this->infoManager->retrieveFor( $target, $targetAddress ),
141            $this->getAuthority()->getUser() );
142        }
143
144        if ( count( $info ) === 0 ) {
145            // Since the IP address only exists in CheckUser, there is no way to access it.
146            // @TODO Allow extensions (like CheckUser) to either pass without a value
147            //      (which would result in a 404) or throw a fatal (which could result in a 403).
148            throw new LocalizedHttpException(
149                new MessageValue( 'ipinfo-rest-log-registered' ),
150                404
151            );
152        }
153
154        return $info;
155    }
156
157    /**
158     * Determine whether the given user is an anonymous or temporary user account.
159     *
160     * @param UserIdentity|null $user The user to check. May be `null`.
161     * @return bool Whether the given user is an anonymous or temporary user.
162     */
163    private function isAnonymousOrTempUser( ?UserIdentity $user ): bool {
164        if ( $user === null ) {
165            return false;
166        }
167
168        return $this->userIdentityUtils->isTemp( $user ) || IPUtils::isValid( $user->getName() );
169    }
170}