1<?php
2/**
3 * Copyright 2017 Facebook, Inc.
4 *
5 * You are hereby granted a non-exclusive, worldwide, royalty-free license to
6 * use, copy, modify, and distribute this software in source code or binary
7 * form for use in connection with the web services and APIs provided by
8 * Facebook.
9 *
10 * As with any software that integrates with the Facebook platform, your use
11 * of this software is subject to the Facebook Developer Principles and
12 * Policies [http://developers.facebook.com/policy/]. This copyright notice
13 * shall be included in all copies or substantial portions of the software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 * DEALINGS IN THE SOFTWARE.
22 *
23 */
24namespace Facebook\Url;
25
26/**
27 * Class FacebookUrlDetectionHandler
28 *
29 * @package Facebook
30 */
31class FacebookUrlDetectionHandler implements UrlDetectionInterface
32{
33    /**
34     * @inheritdoc
35     */
36    public function getCurrentUrl()
37    {
38        return $this->getHttpScheme() . '://' . $this->getHostName() . $this->getServerVar('REQUEST_URI');
39    }
40
41    /**
42     * Get the currently active URL scheme.
43     *
44     * @return string
45     */
46    protected function getHttpScheme()
47    {
48        return $this->isBehindSsl() ? 'https' : 'http';
49    }
50
51    /**
52     * Tries to detect if the server is running behind an SSL.
53     *
54     * @return boolean
55     */
56    protected function isBehindSsl()
57    {
58        // Check for proxy first
59        $protocol = $this->getHeader('X_FORWARDED_PROTO');
60        if ($protocol) {
61            return $this->protocolWithActiveSsl($protocol);
62        }
63
64        $protocol = $this->getServerVar('HTTPS');
65        if ($protocol) {
66            return $this->protocolWithActiveSsl($protocol);
67        }
68
69        return (string)$this->getServerVar('SERVER_PORT') === '443';
70    }
71
72    /**
73     * Detects an active SSL protocol value.
74     *
75     * @param string $protocol
76     *
77     * @return boolean
78     */
79    protected function protocolWithActiveSsl($protocol)
80    {
81        $protocol = strtolower((string)$protocol);
82
83        return in_array($protocol, ['on', '1', 'https', 'ssl'], true);
84    }
85
86    /**
87     * Tries to detect the host name of the server.
88     *
89     * Some elements adapted from
90     *
91     * @see https://github.com/symfony/HttpFoundation/blob/master/Request.php
92     *
93     * @return string
94     */
95    protected function getHostName()
96    {
97        // Check for proxy first
98        $header = $this->getHeader('X_FORWARDED_HOST');
99        if ($header && $this->isValidForwardedHost($header)) {
100            $elements = explode(',', $header);
101            $host = $elements[count($elements) - 1];
102        } elseif (!$host = $this->getHeader('HOST')) {
103            if (!$host = $this->getServerVar('SERVER_NAME')) {
104                $host = $this->getServerVar('SERVER_ADDR');
105            }
106        }
107
108        // trim and remove port number from host
109        // host is lowercase as per RFC 952/2181
110        $host = strtolower(preg_replace('/:\d+$/', '', trim($host)));
111
112        // Port number
113        $scheme = $this->getHttpScheme();
114        $port = $this->getCurrentPort();
115        $appendPort = ':' . $port;
116
117        // Don't append port number if a normal port.
118        if (($scheme == 'http' && $port == '80') || ($scheme == 'https' && $port == '443')) {
119            $appendPort = '';
120        }
121
122        return $host . $appendPort;
123    }
124
125    protected function getCurrentPort()
126    {
127        // Check for proxy first
128        $port = $this->getHeader('X_FORWARDED_PORT');
129        if ($port) {
130            return (string)$port;
131        }
132
133        $protocol = (string)$this->getHeader('X_FORWARDED_PROTO');
134        if ($protocol === 'https') {
135            return '443';
136        }
137
138        return (string)$this->getServerVar('SERVER_PORT');
139    }
140
141    /**
142     * Returns the a value from the $_SERVER super global.
143     *
144     * @param string $key
145     *
146     * @return string
147     */
148    protected function getServerVar($key)
149    {
150        return isset($_SERVER[$key]) ? $_SERVER[$key] : '';
151    }
152
153    /**
154     * Gets a value from the HTTP request headers.
155     *
156     * @param string $key
157     *
158     * @return string
159     */
160    protected function getHeader($key)
161    {
162        return $this->getServerVar('HTTP_' . $key);
163    }
164
165    /**
166     * Checks if the value in X_FORWARDED_HOST is a valid hostname
167     * Could prevent unintended redirections
168     *
169     * @param string $header
170     *
171     * @return boolean
172     */
173    protected function isValidForwardedHost($header)
174    {
175        $elements = explode(',', $header);
176        $host = $elements[count($elements) - 1];
177
178        return preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $host) //valid chars check
179            && 0 < strlen($host) && strlen($host) < 254 //overall length check
180            && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $host); //length of each label
181    }
182}
183