Passed
Push — master ( d7659d...af4024 )
by Pauli
11:57
created

HttpUtil::isUrlSchemeOneOf()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 10
rs 10
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2022, 2023
11
 */
12
13
namespace OCA\Music\Utility;
14
15
/**
16
 * Static utility functions to work with HTTP requests
17
 */
18
class HttpUtil {
19
20
	private const ALLOWED_SCHEMES = ['http', 'https', 'feed', 'podcast', 'pcast', 'podcasts', 'itms-pcast', 'itms-pcasts', 'itms-podcast', 'itms-podcasts'];
21
22
	/**
23
	 * Use HTTP GET to load the requested URL
24
	 * @return array with three keys: ['content' => string|false, 'status_code' => int, 'message' => string]
25
	 */
26
	public static function loadFromUrl(string $url, ?int $maxLength=null, ?int $timeout_s=null) : array {
27
		$status_code = 0;
28
		$content_type = null;
29
30
		if (!self::isUrlSchemeOneOf($url, self::ALLOWED_SCHEMES)) {
31
			$content = false;
32
			$message = 'URL scheme must be one of ' . \json_encode(self::ALLOWED_SCHEMES);
33
		} else {
34
			$opts = [
35
				'http' => [
36
					'header' => self::userAgentHeader(),	// some servers don't allow requests without a user agent header
37
					'ignore_errors' => true,				// don't emit warnings for bad/unavailable URL, we handle errors manually
38
					'max_redirects' => 5
39
				]
40
			];
41
			if ($timeout_s !== null) {
42
				$opts['http']['timeout'] = $timeout_s;
43
			}
44
			$context = \stream_context_create($opts);
45
46
			// The length parameter of file_get_contents isn't nullable prior to PHP8.0
47
			if ($maxLength === null) {
48
				$content = @\file_get_contents($url, false, $context);
49
			} else {
50
				$content = @\file_get_contents($url, false, $context, 0, $maxLength);
51
			}
52
53
			// It's some PHP magic that calling file_get_contents creates and populates also a local
54
			// variable array $http_response_header, provided that the server could be reached.
55
			if (!empty($http_response_header)) {
56
				list($version, $status_code, $message) = \explode(' ', $http_response_header[0], 3);
57
				$status_code = (int)$status_code;
58
				$content_type = self::findHeader($http_response_header, 'Content-Type');
59
			} else {
60
				$message = 'The requested URL did not respond';
61
			}
62
		}
63
64
		return \compact('content', 'status_code', 'message', 'content_type');
65
	}
66
67
	public static function userAgentHeader() : string {
68
		return 'User-Agent: OCMusic/' . AppInfo::getVersion();
69
	}
70
71
	private static function findHeader(array $headers, string $headerKey) : ?string {
72
		foreach ($headers as $header) {
73
			$find = \strstr($header, $headerKey . ':');
74
			if ($find !== false) {
75
				return \trim(\substr($find, \strlen($headerKey)+1));
76
			}
77
		}
78
		return null;
79
	}
80
81
	private static function isUrlSchemeOneOf(string $url, array $schemes) : bool {
82
		$url = \mb_strtolower($url);
83
84
		foreach ($schemes as $scheme) {
85
			if (Util::startsWith($url, $scheme . '://')) {
86
				return true;
87
			}
88
		}
89
90
		return false;
91
	}
92
}
93