]> git.mxchange.org Git - friendica.git/blob - src/App/BaseURL.php
Merge pull request #12039 from nupplaphil/feat/session_util_Modules
[friendica.git] / src / App / BaseURL.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\App;
23
24 use Friendica\Core\Config\Capability\IManageConfigValues;
25 use Friendica\Core\System;
26 use Friendica\Util\Network;
27 use Friendica\Util\Strings;
28 use Friendica\Network\HTTPException;
29
30 /**
31  * A class which checks and contains the basic
32  * environment for the BaseURL (url, urlpath, ssl_policy, hostname, scheme)
33  */
34 class BaseURL
35 {
36         /**
37          * No SSL necessary
38          */
39         const SSL_POLICY_NONE = 0;
40
41         /**
42          * SSL is necessary
43          */
44         const SSL_POLICY_FULL = 1;
45
46         /**
47          * SSL is optional, but preferred
48          */
49         const SSL_POLICY_SELFSIGN = 2;
50
51         /**
52          * Define the Default SSL scheme
53          */
54         const DEFAULT_SSL_SCHEME = self::SSL_POLICY_SELFSIGN;
55
56         /**
57          * The Friendica Config
58          *
59          * @var IManageConfigValues
60          */
61         private $config;
62
63         /**
64          * The server side variables
65          *
66          * @var array
67          */
68         private $server;
69
70         /**
71          * The hostname of the Base URL
72          *
73          * @var string
74          */
75         private $hostname;
76
77         /**
78          * The SSL_POLICY of the Base URL
79          *
80          * @var int
81          */
82         private $sslPolicy;
83
84         /**
85          * The URL sub-path of the Base URL
86          *
87          * @var string
88          */
89         private $urlPath;
90
91         /**
92          * The full URL
93          *
94          * @var string
95          */
96         private $url;
97
98         /**
99          * The current scheme of this call
100          *
101          * @var string
102          */
103         private $scheme;
104
105         /**
106          * Returns the hostname of this node
107          *
108          * @return string
109          */
110         public function getHostname(): string
111         {
112                 return $this->hostname;
113         }
114
115         /**
116          * Returns the current scheme of this call
117          *
118          * @return string
119          */
120         public function getScheme(): string
121         {
122                 return $this->scheme;
123         }
124
125         /**
126          * Returns the SSL policy of this node
127          *
128          * @return int
129          */
130         public function getSSLPolicy(): int
131         {
132                 return $this->sslPolicy;
133         }
134
135         /**
136          * Returns the sub-path of this URL
137          *
138          * @return string
139          */
140         public function getUrlPath(): string
141         {
142                 return $this->urlPath;
143         }
144
145         /**
146          * Returns the full URL of this call
147          *
148          * Note: $ssl parameter value doesn't directly correlate with the resulting protocol
149          *
150          * @param bool $ssl True, if ssl should get used
151          *
152          * @return string
153          */
154         public function get(bool $ssl = false): string
155         {
156                 if ($this->sslPolicy === self::SSL_POLICY_SELFSIGN && $ssl) {
157                         return Network::switchScheme($this->url);
158                 }
159
160                 return $this->url;
161         }
162
163         /**
164          * Save current parts of the base Url
165          *
166          * @param string? $hostname
167          * @param int?    $sslPolicy
168          * @param string? $urlPath
169          *
170          * @return bool true, if successful
171          * @TODO Find proper types
172          */
173         public function save($hostname = null, $sslPolicy = null, $urlPath = null): bool
174         {
175                 $currHostname  = $this->hostname;
176                 $currSSLPolicy = $this->sslPolicy;
177                 $currURLPath   = $this->urlPath;
178
179                 if (!empty($hostname) && $hostname !== $this->hostname) {
180                         if ($this->config->set('config', 'hostname', $hostname)) {
181                                 $this->hostname = $hostname;
182                         } else {
183                                 return false;
184                         }
185                 }
186
187                 if (isset($sslPolicy) && $sslPolicy !== $this->sslPolicy) {
188                         if ($this->config->set('system', 'ssl_policy', $sslPolicy)) {
189                                 $this->sslPolicy = $sslPolicy;
190                         } else {
191                                 $this->hostname = $currHostname;
192                                 $this->config->set('config', 'hostname', $this->hostname);
193                                 return false;
194                         }
195                 }
196
197                 if (isset($urlPath) && $urlPath !== $this->urlPath) {
198                         if ($this->config->set('system', 'urlpath', $urlPath)) {
199                                 $this->urlPath = $urlPath;
200                         } else {
201                                 $this->hostname  = $currHostname;
202                                 $this->sslPolicy = $currSSLPolicy;
203                                 $this->config->set('config', 'hostname', $this->hostname);
204                                 $this->config->set('system', 'ssl_policy', $this->sslPolicy);
205                                 return false;
206                         }
207                 }
208
209                 $this->determineBaseUrl();
210                 if (!$this->config->set('system', 'url', $this->url)) {
211                         $this->hostname  = $currHostname;
212                         $this->sslPolicy = $currSSLPolicy;
213                         $this->urlPath   = $currURLPath;
214                         $this->determineBaseUrl();
215
216                         $this->config->set('config', 'hostname', $this->hostname);
217                         $this->config->set('system', 'ssl_policy', $this->sslPolicy);
218                         $this->config->set('system', 'urlpath', $this->urlPath);
219                         return false;
220                 }
221
222                 return true;
223         }
224
225         /**
226          * Save the current url as base URL
227          *
228          * @param string $url
229          *
230          * @return bool true, if the save was successful
231          */
232         public function saveByURL(string $url): bool
233         {
234                 $parsed = @parse_url($url);
235
236                 if (empty($parsed) || empty($parsed['host'])) {
237                         return false;
238                 }
239
240                 $hostname = $parsed['host'];
241                 if (!empty($hostname) && !empty($parsed['port'])) {
242                         $hostname .= ':' . $parsed['port'];
243                 }
244
245                 $urlPath = null;
246                 if (!empty($parsed['path'])) {
247                         $urlPath = trim($parsed['path'], '\\/');
248                 }
249
250                 $sslPolicy = null;
251                 if (!empty($parsed['scheme'])) {
252                         if ($parsed['scheme'] == 'https') {
253                                 $sslPolicy = BaseURL::SSL_POLICY_FULL;
254                         }
255                 }
256
257                 return $this->save($hostname, $sslPolicy, $urlPath);
258         }
259
260         /**
261          * Checks, if a redirect to the HTTPS site would be necessary
262          *
263          * @return bool
264          */
265         public function checkRedirectHttps()
266         {
267                 return $this->config->get('system', 'force_ssl') &&
268                        ($this->getScheme() == "http") &&
269                        intval($this->getSSLPolicy()) == BaseURL::SSL_POLICY_FULL &&
270                        strpos($this->get(), 'https://') === 0 &&
271                        !empty($this->server['REQUEST_METHOD']) &&
272                        $this->server['REQUEST_METHOD'] === 'GET';
273         }
274
275         /**
276          * @param IManageConfigValues $config The Friendica IConfiguration
277          * @param array               $server The $_SERVER array
278          */
279         public function __construct(IManageConfigValues $config, array $server)
280         {
281                 $this->config = $config;
282                 $this->server = $server;
283
284                 $this->determineSchema();
285                 $this->checkConfig();
286         }
287
288         /**
289          * Check the current config during loading
290          */
291         public function checkConfig()
292         {
293                 $this->hostname  = $this->config->get('config', 'hostname');
294                 $this->urlPath   = $this->config->get('system', 'urlpath');
295                 $this->sslPolicy = $this->config->get('system', 'ssl_policy');
296                 $this->url       = $this->config->get('system', 'url');
297
298                 if (empty($this->hostname)) {
299                         $this->determineHostname();
300
301                         if (!empty($this->hostname)) {
302                                 $this->config->set('config', 'hostname', $this->hostname);
303                         }
304                 }
305
306                 if (!isset($this->urlPath)) {
307                         $this->determineURLPath();
308                         $this->config->set('system', 'urlpath', $this->urlPath);
309                 }
310
311                 if (!isset($this->sslPolicy)) {
312                         if ($this->scheme == 'https') {
313                                 $this->sslPolicy = self::SSL_POLICY_FULL;
314                         } else {
315                                 $this->sslPolicy = self::DEFAULT_SSL_SCHEME;
316                         }
317                         $this->config->set('system', 'ssl_policy', $this->sslPolicy);
318                 }
319
320                 if (empty($this->url)) {
321                         $this->determineBaseUrl();
322
323                         if (!empty($this->url)) {
324                                 $this->config->set('system', 'url', $this->url);
325                         }
326                 }
327         }
328
329         /**
330          * Determines the hostname of this node if not set already
331          */
332         private function determineHostname()
333         {
334                 $this->hostname = '';
335
336                 if (!empty($this->server['SERVER_NAME'])) {
337                         $this->hostname = $this->server['SERVER_NAME'];
338
339                         if (!empty($this->server['SERVER_PORT']) && $this->server['SERVER_PORT'] != 80 && $this->server['SERVER_PORT'] != 443) {
340                                 $this->hostname .= ':' . $this->server['SERVER_PORT'];
341                         }
342                 }
343         }
344
345         /**
346          * Figure out if we are running at the top of a domain or in a sub-directory
347          */
348         private function determineURLPath()
349         {
350                 $this->urlPath = '';
351
352                 /*
353                  * The automatic path detection in this function is currently deactivated,
354                  * see issue https://github.com/friendica/friendica/issues/6679
355                  *
356                  * The problem is that the function seems to be confused with some url.
357                  * These then confuses the detection which changes the url path.
358                  */
359
360                 /* Relative script path to the web server root
361                  * Not all of those $_SERVER properties can be present, so we do by inverse priority order
362                  */
363                 $relative_script_path =
364                         ($this->server['REDIRECT_URL']        ?? '') ?:
365                         ($this->server['REDIRECT_URI']        ?? '') ?:
366                         ($this->server['REDIRECT_SCRIPT_URL'] ?? '') ?:
367                         ($this->server['SCRIPT_URL']          ?? '') ?:
368                          $this->server['REQUEST_URI']         ?? '';
369
370                 /* $relative_script_path gives /relative/path/to/friendica/module/parameter
371                  * QUERY_STRING gives pagename=module/parameter
372                  *
373                  * To get /relative/path/to/friendica we perform dirname() for as many levels as there are slashes in the QUERY_STRING
374                  */
375                 if (!empty($relative_script_path)) {
376                         // Module
377                         if (!empty($this->server['QUERY_STRING'])) {
378                                 $this->urlPath = trim(dirname($relative_script_path, substr_count(trim($this->server['QUERY_STRING'], '/'), '/') + 1), '/');
379                         } else {
380                                 // Root page
381                                 $this->urlPath = trim($relative_script_path, '/');
382                         }
383                 }
384         }
385
386         /**
387          * Determine the full URL based on all parts
388          */
389         private function determineBaseUrl()
390         {
391                 $scheme = 'http';
392
393                 if ($this->sslPolicy == self::SSL_POLICY_FULL) {
394                         $scheme = 'https';
395                 }
396
397                 $this->url = $scheme . '://' . $this->hostname . (!empty($this->urlPath) ? '/' . $this->urlPath : '');
398         }
399
400         /**
401          * Determine the scheme of the current used link
402          */
403         private function determineSchema()
404         {
405                 $this->scheme = 'http';
406
407                 if (!empty($this->server['HTTPS']) ||
408                     !empty($this->server['HTTP_FORWARDED']) && preg_match('/proto=https/', $this->server['HTTP_FORWARDED']) ||
409                     !empty($this->server['HTTP_X_FORWARDED_PROTO']) && $this->server['HTTP_X_FORWARDED_PROTO'] == 'https' ||
410                     !empty($this->server['HTTP_X_FORWARDED_SSL']) && $this->server['HTTP_X_FORWARDED_SSL'] == 'on' ||
411                     !empty($this->server['FRONT_END_HTTPS']) && $this->server['FRONT_END_HTTPS'] == 'on' ||
412                     !empty($this->server['SERVER_PORT']) && (intval($this->server['SERVER_PORT']) == 443) // XXX: reasonable assumption, but isn't this hardcoding too much?
413                 ) {
414                         $this->scheme = 'https';
415                 }
416         }
417
418         /**
419          * Removes the base url from an url. This avoids some mixed content problems.
420          *
421          * @param string $origURL
422          *
423          * @return string The cleaned url
424          */
425         public function remove(string $origURL): string
426         {
427                 // Remove the hostname from the url if it is an internal link
428                 $nurl = Strings::normaliseLink($origURL);
429                 $base = Strings::normaliseLink($this->get());
430                 $url  = str_replace($base . '/', '', $nurl);
431
432                 // if it is an external link return the orignal value
433                 if ($url == Strings::normaliseLink($origURL)) {
434                         return $origURL;
435                 } else {
436                         return $url;
437                 }
438         }
439
440         /**
441          * Redirects to another module relative to the current Friendica base URL.
442          * If you want to redirect to a external URL, use System::externalRedirectTo()
443          *
444          * @param string $toUrl The destination URL (Default is empty, which is the default page of the Friendica node)
445          * @param bool   $ssl   if true, base URL will try to get called with https:// (works just for relative paths)
446          *
447          * @throws HTTPException\FoundException
448          * @throws HTTPException\MovedPermanentlyException
449          * @throws HTTPException\TemporaryRedirectException
450          *
451          * @throws HTTPException\InternalServerErrorException In Case the given URL is not relative to the Friendica node
452          */
453         public function redirect(string $toUrl = '', bool $ssl = false)
454         {
455                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
456                         throw new HTTPException\InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo");
457                 }
458
459                 $redirectTo = $this->get($ssl) . '/' . ltrim($toUrl, '/');
460                 System::externalRedirect($redirectTo);
461         }
462
463         /**
464          * Returns the base url as string
465          */
466         public function __toString(): string
467         {
468                 return (string) $this->get();
469         }
470 }