Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.36% covered (warning)
77.36%
41 / 53
75.00% covered (warning)
75.00%
9 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
MWRestrictions
77.36% covered (warning)
77.36%
41 / 53
75.00% covered (warning)
75.00%
9 / 12
33.85
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 newDefault
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromJson
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 loadFromArray
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
9.08
 toArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 toJson
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 check
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 userCan
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 checkIP
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 checkPage
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * A class to check request restrictions expressed as a JSON object
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
21use MediaWiki\Json\FormatJson;
22use MediaWiki\Linker\LinkTarget;
23use MediaWiki\Request\WebRequest;
24use MediaWiki\Status\Status;
25use MediaWiki\Title\Title;
26use Wikimedia\IPSet;
27use Wikimedia\IPUtils;
28
29/**
30 * A class to check request restrictions expressed as a JSON object
31 */
32class MWRestrictions implements Stringable {
33
34    /** @var string[] */
35    private $ipAddresses = [ '0.0.0.0/0', '::/0' ];
36
37    /** @var string[] */
38    private $pages = [];
39
40    public StatusValue $validity;
41
42    /**
43     * @param array|null $restrictions
44     * @throws InvalidArgumentException
45     */
46    protected function __construct( array $restrictions = null ) {
47        $this->validity = StatusValue::newGood();
48        if ( $restrictions !== null ) {
49            $this->loadFromArray( $restrictions );
50        }
51    }
52
53    /**
54     * @return MWRestrictions
55     */
56    public static function newDefault() {
57        return new self();
58    }
59
60    /**
61     * @param array $restrictions
62     * @return MWRestrictions
63     * @throws InvalidArgumentException
64     */
65    public static function newFromArray( array $restrictions ) {
66        return new self( $restrictions );
67    }
68
69    /**
70     * @param string $json JSON representation of the restrictions
71     * @return MWRestrictions
72     * @throws InvalidArgumentException
73     */
74    public static function newFromJson( $json ) {
75        $restrictions = FormatJson::decode( $json, true );
76        if ( !is_array( $restrictions ) ) {
77            throw new InvalidArgumentException( 'Invalid restrictions JSON' );
78        }
79        return new self( $restrictions );
80    }
81
82    private function loadFromArray( array $restrictions ) {
83        static $neededKeys = [ 'IPAddresses' ];
84
85        $keys = array_keys( $restrictions );
86        $missingKeys = array_diff( $neededKeys, $keys );
87        if ( $missingKeys ) {
88            throw new InvalidArgumentException(
89                'Array is missing required keys: ' . implode( ', ', $missingKeys )
90            );
91        }
92
93        if ( !is_array( $restrictions['IPAddresses'] ) ) {
94            throw new InvalidArgumentException( 'IPAddresses is not an array' );
95        }
96        foreach ( $restrictions['IPAddresses'] as $ip ) {
97            if ( !IPUtils::isIPAddress( $ip ) ) {
98                $this->validity->fatal( 'restrictionsfield-badip', $ip );
99            }
100        }
101        $this->ipAddresses = $restrictions['IPAddresses'];
102
103        if ( isset( $restrictions['Pages'] ) ) {
104            if ( !is_array( $restrictions['Pages'] ) ) {
105                throw new InvalidArgumentException( 'Pages is not an array of page names' );
106            }
107            foreach ( $restrictions['Pages'] as $page ) {
108                if ( !is_string( $page ) ) {
109                    throw new InvalidArgumentException( "Pages contains non-string value: $page" );
110                }
111            }
112            $this->pages = $restrictions['Pages'];
113        }
114    }
115
116    /**
117     * Return the restrictions as an array
118     * @return array
119     */
120    public function toArray() {
121        $arr = [ 'IPAddresses' => $this->ipAddresses ];
122        if ( count( $this->pages ) ) {
123            $arr['Pages'] = $this->pages;
124        }
125        return $arr;
126    }
127
128    /**
129     * Return the restrictions as a JSON string
130     * @param bool|string $pretty Pretty-print the JSON output, see FormatJson::encode
131     * @return string
132     */
133    public function toJson( $pretty = false ) {
134        return FormatJson::encode( $this->toArray(), $pretty, FormatJson::ALL_OK );
135    }
136
137    public function __toString() {
138        return $this->toJson();
139    }
140
141    /**
142     * Test against the passed WebRequest
143     * @param WebRequest $request
144     * @return Status
145     */
146    public function check( WebRequest $request ) {
147        $ok = [
148            'ip' => $this->checkIP( $request->getIP() ),
149        ];
150        $status = Status::newGood();
151        $status->setResult( $ok === array_filter( $ok ), $ok );
152        return $status;
153    }
154
155    /**
156     * Test whether an action on the target is allowed by the restrictions
157     *
158     * @internal
159     * @param LinkTarget $target
160     * @return StatusValue
161     */
162    public function userCan( LinkTarget $target ) {
163        if ( !$this->checkPage( $target ) ) {
164            return StatusValue::newFatal( 'session-page-restricted' );
165        }
166        return StatusValue::newGood();
167    }
168
169    /**
170     * Test if an IP address is allowed by the restrictions
171     * @param string $ip
172     * @return bool
173     */
174    public function checkIP( $ip ) {
175        $set = new IPSet( $this->ipAddresses );
176        return $set->match( $ip );
177    }
178
179    /**
180     * Test if an action on a title is allowed by the restrictions
181     *
182     * @param LinkTarget $target
183     * @return bool
184     */
185    private function checkPage( LinkTarget $target ) {
186        if ( count( $this->pages ) === 0 ) {
187            return true;
188        }
189        $pagesNormalized = array_map( static function ( $titleText ) {
190            $title = Title::newFromText( $titleText );
191            return $title ? $title->getPrefixedText() : '';
192        }, $this->pages );
193        return in_array( Title::newFromLinkTarget( $target )->getPrefixedText(), $pagesNormalized, true );
194    }
195}