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