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