Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.37% covered (success)
97.37%
37 / 38
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Origin
97.37% covered (success)
97.37%
37 / 38
90.00% covered (success)
90.00%
9 / 10
20
0.00% covered (danger)
0.00%
0 / 1
 parseHeaderList
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 isNullOrigin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isMultiOrigin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOriginList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSingleOrigin
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 match
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 matchSingleOrigin
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 execute
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 wildcardToRegex
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Rest\HeaderParser;
4
5use Wikimedia\Assert\Assert;
6
7/**
8 * A class to assist with the parsing of Origin header according to the RFC 6454
9 * @link https://tools.ietf.org/html/rfc6454#section-7
10 * @since 1.36
11 */
12class Origin extends HeaderParserBase {
13
14    public const HEADER_NAME = 'Origin';
15
16    /** @var bool whether the origin was set to null */
17    private $isNullOrigin;
18
19    /** @var array List of specified origins */
20    private $origins = [];
21
22    /**
23     * Parse an Origin header list as returned by RequestInterface::getHeader().
24     *
25     * @param string[] $headerList
26     * @return self
27     */
28    public static function parseHeaderList( array $headerList ): self {
29        $parser = new self( $headerList );
30        $parser->execute();
31        return $parser;
32    }
33
34    /**
35     * Whether the Origin header was explicitly set to `null`.
36     *
37     * @return bool
38     */
39    public function isNullOrigin(): bool {
40        return $this->isNullOrigin;
41    }
42
43    /**
44     * Whether the Origin header contains multiple origins.
45     *
46     * @return bool
47     */
48    public function isMultiOrigin(): bool {
49        return count( $this->getOriginList() ) > 1;
50    }
51
52    /**
53     * Get the list of origins.
54     *
55     * @return string[]
56     */
57    public function getOriginList(): array {
58        return $this->origins;
59    }
60
61    /**
62     * @return string
63     */
64    public function getSingleOrigin(): string {
65        Assert::precondition( !$this->isMultiOrigin(),
66            'Cannot get single origin, header specifies multiple' );
67        return $this->getOriginList()[0];
68    }
69
70    /**
71     * Check whether all the origins match at least one of the rules in $allowList.
72     *
73     * @param string[] $allowList
74     * @param string[] $excludeList
75     * @return bool
76     */
77    public function match( array $allowList, array $excludeList ): bool {
78        if ( $this->isNullOrigin() ) {
79            return false;
80        }
81
82        foreach ( $this->getOriginList() as $origin ) {
83            if ( !self::matchSingleOrigin( $origin, $allowList, $excludeList ) ) {
84                return false;
85            }
86        }
87        return true;
88    }
89
90    /**
91     * Checks whether the origin matches at list one of the provided rules in $allowList.
92     *
93     * @param string $origin
94     * @param array $allowList
95     * @param array $excludeList
96     * @return bool
97     */
98    private static function matchSingleOrigin( string $origin, array $allowList, array $excludeList ): bool {
99        foreach ( $allowList as $rule ) {
100            if ( preg_match( self::wildcardToRegex( $rule ), $origin ) ) {
101                // Rule matches, check exceptions
102                foreach ( $excludeList as $exc ) {
103                    if ( preg_match( self::wildcardToRegex( $exc ), $origin ) ) {
104                        return false;
105                    }
106                }
107
108                return true;
109            }
110        }
111
112        return false;
113    }
114
115    /**
116     * Private constructor. Use the public static functions for public access.
117     *
118     * @param string[] $input
119     */
120    private function __construct( array $input ) {
121        if ( count( $input ) !== 1 ) {
122            $this->error( 'Only a single Origin header field allowed in HTTP request' );
123        }
124        $this->setInput( trim( $input[0] ) );
125    }
126
127    private function execute() {
128        if ( $this->input === 'null' ) {
129            $this->isNullOrigin = true;
130        } else {
131            $this->isNullOrigin = false;
132            $this->origins = preg_split( '/\s+/', $this->input );
133            if ( count( $this->origins ) === 0 ) {
134                $this->error( 'Origin header must contain at least one origin' );
135            }
136        }
137    }
138
139    /**
140     * Helper function to convert wildcard string into a regex
141     * '*' => '.*?'
142     * '?' => '.'
143     *
144     * @param string $wildcard String with wildcards
145     * @return string Regular expression
146     */
147    private static function wildcardToRegex( $wildcard ) {
148        $wildcard = preg_quote( $wildcard, '/' );
149        $wildcard = str_replace(
150            [ '\*', '\?' ],
151            [ '.*?', '.' ],
152            $wildcard
153        );
154
155        return "/^https?:\/\/$wildcard$/";
156    }
157}