Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.14% covered (warning)
67.14%
47 / 70
36.84% covered (danger)
36.84%
7 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
CompositeBlock
67.14% covered (warning)
67.14%
47 / 70
36.84% covered (danger)
36.84%
7 / 19
92.95
0.00% covered (danger)
0.00%
0 / 1
 createFromBlocks
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 propHasValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 methodReturnsValue
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 getOriginalBlocks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withOriginalBlocks
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 toArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTimestamp
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 getExpiry
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 getIdentifier
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 appliesToRight
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 appliesToUsertalk
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 appliesToTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 appliesToNamespace
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 appliesToPage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 appliesToPasswordReset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBy
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getByName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBlocker
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Class for blocks composed from multiple blocks.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23namespace MediaWiki\Block;
24
25use InvalidArgumentException;
26use MediaWiki\Message\Message;
27use MediaWiki\Title\Title;
28use MediaWiki\User\UserIdentity;
29
30/**
31 * Multiple Block class.
32 *
33 * Multiple blocks exist to enforce restrictions from more than one block, if several
34 * blocks apply to a user/IP. Multiple blocks are created temporarily on enforcement.
35 *
36 * @since 1.34
37 */
38class CompositeBlock extends AbstractBlock {
39    /** @var AbstractBlock[] */
40    private $originalBlocks;
41
42    /**
43     * Helper method for merging multiple blocks into a composite block.
44     * @param AbstractBlock ...$blocks
45     * @return self
46     */
47    public static function createFromBlocks( AbstractBlock ...$blocks ): self {
48        $originalBlocks = [];
49        foreach ( $blocks as $block ) {
50            if ( $block instanceof self ) {
51                $originalBlocks = array_merge( $originalBlocks, $block->getOriginalBlocks() );
52            } else {
53                $originalBlocks[] = $block;
54            }
55        }
56        if ( !$originalBlocks ) {
57            throw new InvalidArgumentException( 'No blocks given' );
58        }
59        return new self( [
60            'address' => $originalBlocks[0]->target,
61            'reason' => new Message( 'blockedtext-composite-reason' ),
62            'originalBlocks' => $originalBlocks,
63        ] );
64    }
65
66    /**
67     * Create a new block with specified parameters on a user, IP or IP range.
68     *
69     * @param array $options Parameters of the block, with options supported by
70     *  `AbstractBlock::__construct`, and also:
71     *  - originalBlocks: (Block[]) Blocks that this block is composed from
72     */
73    public function __construct( array $options = [] ) {
74        parent::__construct( $options );
75
76        $defaults = [
77            'originalBlocks' => [],
78        ];
79
80        $options += $defaults;
81
82        $this->originalBlocks = $options[ 'originalBlocks' ];
83
84        $this->setHideName( $this->propHasValue( 'hideName', true ) );
85        $this->isHardblock( $this->propHasValue( 'isHardblock', true ) );
86        $this->isSitewide( $this->propHasValue( 'isSitewide', true ) );
87        $this->isEmailBlocked( $this->propHasValue( 'blockEmail', true ) );
88        $this->isCreateAccountBlocked( $this->propHasValue( 'blockCreateAccount', true ) );
89        $this->isUsertalkEditAllowed( !$this->propHasValue( 'allowUsertalk', false ) );
90    }
91
92    /**
93     * Determine whether any original blocks have a particular property set to a
94     * particular value.
95     *
96     * @param string $prop
97     * @param mixed $value
98     * @return bool At least one block has the property set to the value
99     */
100    private function propHasValue( $prop, $value ) {
101        foreach ( $this->originalBlocks as $block ) {
102            if ( $block->$prop == $value ) {
103                return true;
104            }
105        }
106        return false;
107    }
108
109    /**
110     * Determine whether any original blocks have a particular method returning a
111     * particular value.
112     *
113     * @param string $method
114     * @param mixed $value
115     * @param mixed ...$params
116     * @return bool At least one block has the method returning the value
117     */
118    private function methodReturnsValue( $method, $value, ...$params ) {
119        foreach ( $this->originalBlocks as $block ) {
120            if ( $block->$method( ...$params ) == $value ) {
121                return true;
122            }
123        }
124        return false;
125    }
126
127    /**
128     * Get the original blocks from which this block is composed
129     *
130     * @since 1.34
131     * @return AbstractBlock[]
132     */
133    public function getOriginalBlocks() {
134        return $this->originalBlocks;
135    }
136
137    /**
138     * Create a clone of the object with the original blocks array set to
139     * something else.
140     *
141     * @since 1.42
142     * @param AbstractBlock[] $blocks
143     * @return self
144     */
145    public function withOriginalBlocks( array $blocks ) {
146        $clone = clone $this;
147        $clone->originalBlocks = $blocks;
148        return $clone;
149    }
150
151    public function toArray(): array {
152        return $this->originalBlocks;
153    }
154
155    /**
156     * @inheritDoc
157     */
158    public function getTimestamp(): string {
159        $minStart = null;
160        foreach ( $this->originalBlocks as $block ) {
161            $startTime = $block->getTimestamp();
162            if ( $minStart === null || $startTime === '' || $startTime < $minStart ) {
163                $minStart = $startTime;
164            }
165        }
166        return $minStart ?? '';
167    }
168
169    /**
170     * @inheritDoc
171     */
172    public function getExpiry(): string {
173        $maxExpiry = null;
174        foreach ( $this->originalBlocks as $block ) {
175            $expiry = $block->getExpiry();
176            if ( $maxExpiry === null || $expiry === '' || $expiry > $maxExpiry ) {
177                $maxExpiry = $expiry;
178            }
179        }
180        return $maxExpiry ?? '';
181    }
182
183    /**
184     * @inheritDoc
185     */
186    public function getIdentifier( $wikiId = self::LOCAL ) {
187        $identifier = [];
188        foreach ( $this->originalBlocks as $block ) {
189            $identifier[] = $block->getIdentifier( $wikiId );
190        }
191        return $identifier;
192    }
193
194    /**
195     * @inheritDoc
196     *
197     * Determines whether the CompositeBlock applies to a right by checking
198     * whether the original blocks apply to that right. Each block can report
199     * true (applies), false (does not apply) or null (unsure). Then:
200     * - If any original blocks apply, this block applies
201     * - If no original blocks apply but any are unsure, this block is unsure
202     * - If all blocks do not apply, this block does not apply
203     */
204    public function appliesToRight( $right ) {
205        $isUnsure = false;
206
207        foreach ( $this->originalBlocks as $block ) {
208            $appliesToRight = $block->appliesToRight( $right );
209
210            if ( $appliesToRight ) {
211                return true;
212            } elseif ( $appliesToRight === null ) {
213                $isUnsure = true;
214            }
215        }
216
217        return $isUnsure ? null : false;
218    }
219
220    /**
221     * @inheritDoc
222     */
223    public function appliesToUsertalk( Title $usertalk = null ) {
224        return $this->methodReturnsValue( __FUNCTION__, true, $usertalk );
225    }
226
227    /**
228     * @inheritDoc
229     */
230    public function appliesToTitle( Title $title ) {
231        return $this->methodReturnsValue( __FUNCTION__, true, $title );
232    }
233
234    /**
235     * @inheritDoc
236     */
237    public function appliesToNamespace( $ns ) {
238        return $this->methodReturnsValue( __FUNCTION__, true, $ns );
239    }
240
241    /**
242     * @inheritDoc
243     */
244    public function appliesToPage( $pageId ) {
245        return $this->methodReturnsValue( __FUNCTION__, true, $pageId );
246    }
247
248    /**
249     * @inheritDoc
250     */
251    public function appliesToPasswordReset() {
252        return $this->methodReturnsValue( __FUNCTION__, true );
253    }
254
255    /**
256     * @inheritDoc
257     */
258    public function getBy( $wikiId = self::LOCAL ): int {
259        $this->assertWiki( $wikiId );
260        return 0;
261    }
262
263    /**
264     * @inheritDoc
265     */
266    public function getByName() {
267        return '';
268    }
269
270    /**
271     * @inheritDoc
272     */
273    public function getBlocker(): ?UserIdentity {
274        return null;
275    }
276}