Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.32% covered (warning)
81.32%
74 / 91
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
UnblockUser
81.32% covered (warning)
81.32%
74 / 91
40.00% covered (danger)
40.00%
2 / 5
29.07
0.00% covered (danger)
0.00%
0 / 1
 __construct
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
3.00
 unblock
63.64% covered (warning)
63.64%
14 / 22
0.00% covered (danger)
0.00%
0 / 1
9.36
 unblockUnsafe
69.23% covered (warning)
69.23%
18 / 26
0.00% covered (danger)
0.00%
0 / 1
12.91
 log
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 getBlockToRemove
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
18 *
19 * @file
20 */
21
22namespace MediaWiki\Block;
23
24use ChangeTags;
25use MediaWiki\HookContainer\HookContainer;
26use MediaWiki\HookContainer\HookRunner;
27use MediaWiki\Logging\ManualLogEntry;
28use MediaWiki\Message\Message;
29use MediaWiki\Permissions\Authority;
30use MediaWiki\Status\Status;
31use MediaWiki\User\UserFactory;
32use MediaWiki\User\UserIdentity;
33use RevisionDeleteUser;
34
35/**
36 * Backend class for unblocking users
37 *
38 * @since 1.36
39 */
40class UnblockUser {
41    private BlockPermissionChecker $blockPermissionChecker;
42    private DatabaseBlockStore $blockStore;
43    private BlockTargetFactory $blockTargetFactory;
44    private UserFactory $userFactory;
45    private HookRunner $hookRunner;
46
47    /** @var BlockTarget|null */
48    private $target;
49
50    private ?DatabaseBlock $block;
51
52    private ?DatabaseBlock $blockToRemove = null;
53
54    /** @var Authority */
55    private $performer;
56
57    /** @var string */
58    private $reason;
59
60    /** @var array */
61    private $tags = [];
62
63    /**
64     * @param BlockPermissionCheckerFactory $blockPermissionCheckerFactory
65     * @param DatabaseBlockStore $blockStore
66     * @param BlockTargetFactory $blockTargetFactory
67     * @param UserFactory $userFactory
68     * @param HookContainer $hookContainer
69     * @param DatabaseBlock|null $blockToRemove
70     * @param UserIdentity|string|null $target
71     * @param Authority $performer
72     * @param string $reason
73     * @param string[] $tags
74     */
75    public function __construct(
76        BlockPermissionCheckerFactory $blockPermissionCheckerFactory,
77        DatabaseBlockStore $blockStore,
78        BlockTargetFactory $blockTargetFactory,
79        UserFactory $userFactory,
80        HookContainer $hookContainer,
81        ?DatabaseBlock $blockToRemove,
82        $target,
83        Authority $performer,
84        string $reason,
85        array $tags = []
86    ) {
87        // Process dependencies
88        $this->blockStore = $blockStore;
89        $this->blockTargetFactory = $blockTargetFactory;
90        $this->userFactory = $userFactory;
91        $this->hookRunner = new HookRunner( $hookContainer );
92
93        // Process params
94        if ( $blockToRemove !== null ) {
95            $this->blockToRemove = $blockToRemove;
96            $this->target = $blockToRemove->getTarget();
97        } elseif ( $target instanceof BlockTarget ) {
98            $this->target = $target;
99        } else {
100            // TODO: deprecate (T382106)
101            $this->target = $this->blockTargetFactory->newFromLegacyUnion( $target );
102        }
103
104        $this->blockPermissionChecker = $blockPermissionCheckerFactory
105            ->newChecker(
106                $performer
107            );
108
109        $this->performer = $performer;
110        $this->reason = $reason;
111        $this->tags = $tags;
112    }
113
114    /**
115     * Unblock user
116     */
117    public function unblock(): Status {
118        $status = Status::newGood();
119
120        $this->block = $this->getBlockToRemove( $status );
121        if ( !$status->isOK() ) {
122            return $status;
123        }
124
125        $blockPermissionCheckResult = $this->blockPermissionChecker->checkBlockPermissions( $this->target );
126        if ( $blockPermissionCheckResult !== true ) {
127            return $status->fatal( $blockPermissionCheckResult );
128        }
129
130        $basePermissionCheckResult = $this->blockPermissionChecker->checkBasePermissions(
131            $this->block instanceof DatabaseBlock && $this->block->getHideName()
132        );
133
134        if ( $basePermissionCheckResult !== true ) {
135            return $status->fatal( $basePermissionCheckResult );
136        }
137
138        if ( count( $this->tags ) !== 0 ) {
139            $status->merge(
140                ChangeTags::canAddTagsAccompanyingChange(
141                    $this->tags,
142                    $this->performer
143                )
144            );
145        }
146
147        if ( !$status->isOK() ) {
148            return $status;
149        }
150
151        return $this->unblockUnsafe();
152    }
153
154    /**
155     * Unblock user without any sort of permission checks
156     *
157     * @internal This is public to be called in a maintenance script.
158     * @return Status
159     */
160    public function unblockUnsafe(): Status {
161        $status = Status::newGood();
162
163        $this->block ??= $this->getBlockToRemove( $status );
164        if ( !$status->isOK() ) {
165            return $status;
166        }
167
168        if ( $this->block === null ) {
169            return $status->fatal( 'ipb_cant_unblock', $this->target );
170        }
171
172        if (
173            $this->block->getType() === AbstractBlock::TYPE_RANGE &&
174            $this->target->getType() === AbstractBlock::TYPE_IP
175        ) {
176            return $status->fatal( 'ipb_blocked_as_range', $this->target, $this->block->getTargetName() );
177        }
178
179        $denyReason = [ 'hookaborted' ];
180        $legacyUser = $this->userFactory->newFromAuthority( $this->performer );
181        if ( !$this->hookRunner->onUnblockUser( $this->block, $legacyUser, $denyReason ) ) {
182            foreach ( $denyReason as $key ) {
183                $status->fatal( $key );
184            }
185            return $status;
186        }
187
188        $deleteBlockStatus = $this->blockStore->deleteBlock( $this->block );
189        if ( !$deleteBlockStatus ) {
190            return $status->fatal( 'ipb_cant_unblock', $this->block->getTargetName() );
191        }
192
193        $this->hookRunner->onUnblockUserComplete( $this->block, $legacyUser );
194
195        // Unset _deleted fields as needed
196        $user = $this->block->getTargetUserIdentity();
197        if ( $this->block->getHideName() && $user ) {
198            $id = $user->getId();
199            RevisionDeleteUser::unsuppressUserName( $user->getName(), $id );
200        }
201
202        $this->log();
203
204        $status->setResult( $status->isOK(), $this->block );
205        return $status;
206    }
207
208    /**
209     * Log the unblock to Special:Log/block
210     */
211    private function log() {
212        $page = $this->block->getRedactedTarget()->getLogPage();
213        $logEntry = new ManualLogEntry( 'block', 'unblock' );
214
215        $logEntry->setTarget( $page );
216        $logEntry->setComment( $this->reason );
217        $logEntry->setPerformer( $this->performer->getUser() );
218        $logEntry->addTags( $this->tags );
219        $logEntry->setRelations( [ 'ipb_id' => $this->block->getId() ] );
220        // Save the ID to log_params, since MW 1.44
221        $logEntry->addParameter( 'blockId', $this->block->getId() );
222        $logId = $logEntry->insert();
223        $logEntry->publish( $logId );
224    }
225
226    private function getBlockToRemove( Status $status ): ?DatabaseBlock {
227        if ( $this->blockToRemove !== null ) {
228            return $this->blockToRemove;
229        }
230
231        $activeBlocks = $this->blockStore->newListFromTarget( $this->target );
232        if ( !$activeBlocks ) {
233            $status->fatal( 'ipb_cant_unblock', $this->target );
234            return null;
235        }
236
237        if ( count( $activeBlocks ) > 1 ) {
238            $status->fatal( 'ipb_cant_unblock_multiple_blocks',
239                count( $activeBlocks ), Message::listParam(
240                    array_map(
241                        static function ( $block ) {
242                            return $block->getId();
243                        }, $activeBlocks )
244                )
245            );
246        }
247
248        return $activeBlocks[0];
249    }
250}