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