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