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    public function isNullOrigin(): bool {
38        return $this->isNullOrigin;
39    }
40
41    /**
42     * Whether the Origin header contains multiple origins.
43     */
44    public function isMultiOrigin(): bool {
45        return count( $this->getOriginList() ) > 1;
46    }
47
48    /**
49     * Get the list of origins.
50     *
51     * @return string[]
52     */
53    public function getOriginList(): array {
54        return $this->origins;
55    }
56
57    public function getSingleOrigin(): string {
58        Assert::precondition( !$this->isMultiOrigin(),
59            'Cannot get single origin, header specifies multiple' );
60        return $this->getOriginList()[0];
61    }
62
63    /**
64     * Check whether all the origins match at least one of the rules in $allowList.
65     *
66     * @param string[] $allowList
67     * @param string[] $excludeList
68     * @return bool
69     */
70    public function match( array $allowList, array $excludeList ): bool {
71        if ( $this->isNullOrigin() ) {
72            return false;
73        }
74
75        foreach ( $this->getOriginList() as $origin ) {
76            if ( !self::matchSingleOrigin( $origin, $allowList, $excludeList ) ) {
77                return false;
78            }
79        }
80        return true;
81    }
82
83    /**
84     * Checks whether the origin matches at list one of the provided rules in $allowList.
85     *
86     * @param string $origin
87     * @param array $allowList
88     * @param array $excludeList
89     * @return bool
90     */
91    private static function matchSingleOrigin( string $origin, array $allowList, array $excludeList ): bool {
92        foreach ( $allowList as $rule ) {
93            if ( preg_match( self::wildcardToRegex( $rule ), $origin ) ) {
94                // Rule matches, check exceptions
95                foreach ( $excludeList as $exc ) {
96                    if ( preg_match( self::wildcardToRegex( $exc ), $origin ) ) {
97                        return false;
98                    }
99                }
100
101                return true;
102            }
103        }
104
105        return false;
106    }
107
108    /**
109     * Private constructor. Use the public static functions for public access.
110     *
111     * @param string[] $input
112     */
113    private function __construct( array $input ) {
114        if ( count( $input ) !== 1 ) {
115            $this->error( 'Only a single Origin header field allowed in HTTP request' );
116        }
117        $this->setInput( trim( $input[0] ) );
118    }
119
120    private function execute() {
121        if ( $this->input === 'null' ) {
122            $this->isNullOrigin = true;
123        } else {
124            $this->isNullOrigin = false;
125            $this->origins = preg_split( '/\s+/', $this->input );
126            if ( count( $this->origins ) === 0 ) {
127                $this->error( 'Origin header must contain at least one origin' );
128            }
129        }
130    }
131
132    /**
133     * Helper function to convert wildcard string into a regex
134     * '*' => '.*?'
135     * '?' => '.'
136     *
137     * @param string $wildcard String with wildcards
138     * @return string Regular expression
139     */
140    private static function wildcardToRegex( $wildcard ) {
141        $wildcard = preg_quote( $wildcard, '/' );
142        $wildcard = str_replace(
143            [ '\*', '\?' ],
144            [ '.*?', '.' ],
145            $wildcard
146        );
147
148        return "/^https?:\/\/$wildcard$/";
149    }
150}