Passed
Push — master ( 1e85cb...6741b3 )
by Joas
16:42 queued 18s
created

LinkReferenceProvider::resolveReference()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * @copyright Copyright (c) 2022 Julius Härtl <[email protected]>
6
 *
7
 * @author Julius Härtl <[email protected]>
8
 *
9
 * @license GNU AGPL version 3 or any later version
10
 *
11
 * This program is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License as
13
 * published by the Free Software Foundation, either version 3 of the
14
 * License, or (at your option) any later version.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
 * GNU Affero General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU Affero General Public License
22
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
 */
24
25
namespace OC\Collaboration\Reference;
26
27
use Fusonic\OpenGraph\Consumer;
28
use GuzzleHttp\Exception\GuzzleException;
29
use GuzzleHttp\Psr7\LimitStream;
30
use GuzzleHttp\Psr7\Utils;
31
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
32
use OC\Security\RateLimiting\Limiter;
33
use OC\SystemConfig;
34
use OCP\Collaboration\Reference\IReference;
35
use OCP\Collaboration\Reference\IReferenceProvider;
36
use OCP\Collaboration\Reference\Reference;
37
use OCP\Files\AppData\IAppDataFactory;
38
use OCP\Files\NotFoundException;
39
use OCP\Http\Client\IClientService;
40
use OCP\IRequest;
41
use OCP\IURLGenerator;
42
use OCP\IUserSession;
43
use Psr\Log\LoggerInterface;
44
45
class LinkReferenceProvider implements IReferenceProvider {
46
	public const MAX_PREVIEW_SIZE = 1024 * 1024;
47
48
	public const ALLOWED_CONTENT_TYPES = [
49
		'image/png',
50
		'image/jpg',
51
		'image/jpeg',
52
		'image/gif',
53
		'image/svg+xml',
54
		'image/webp'
55
	];
56
57
	private IClientService $clientService;
58
	private LoggerInterface $logger;
59
	private SystemConfig $systemConfig;
60
	private IAppDataFactory $appDataFactory;
61
	private IURLGenerator $urlGenerator;
62
	private Limiter $limiter;
63
	private IUserSession $userSession;
64
	private IRequest $request;
65
66
	public function __construct(IClientService $clientService, LoggerInterface $logger, SystemConfig $systemConfig, IAppDataFactory $appDataFactory, IURLGenerator $urlGenerator, Limiter $limiter, IUserSession $userSession, IRequest $request) {
67
		$this->clientService = $clientService;
68
		$this->logger = $logger;
69
		$this->systemConfig = $systemConfig;
70
		$this->appDataFactory = $appDataFactory;
71
		$this->urlGenerator = $urlGenerator;
72
		$this->limiter = $limiter;
73
		$this->userSession = $userSession;
74
		$this->request = $request;
75
	}
76
77
	public function matchReference(string $referenceText): bool {
78
		if ($this->systemConfig->getValue('reference_opengraph', true) !== true) {
79
			return false;
80
		}
81
82
		return (bool)preg_match(IURLGenerator::URL_REGEX, $referenceText);
83
	}
84
85
	public function resolveReference(string $referenceText): ?IReference {
86
		if ($this->matchReference($referenceText)) {
87
			$reference = new Reference($referenceText);
88
			$this->fetchReference($reference);
89
			return $reference;
90
		}
91
92
		return null;
93
	}
94
95
	private function fetchReference(Reference $reference): void {
96
		try {
97
			$user = $this->userSession->getUser();
98
			if ($user) {
99
				$this->limiter->registerUserRequest('opengraph', 10, 120, $user);
100
			} else {
101
				$this->limiter->registerAnonRequest('opengraph', 10, 120, $this->request->getRemoteAddress());
102
			}
103
		} catch (RateLimitExceededException $e) {
104
			return;
105
		}
106
107
		$client = $this->clientService->newClient();
108
		try {
109
			$headResponse = $client->head($reference->getId(), [ 'timeout' => 10 ]);
110
		} catch (\Exception $e) {
111
			$this->logger->debug('Failed to perform HEAD request to get target metadata', ['exception' => $e]);
112
			return;
113
		}
114
		$linkContentLength = $headResponse->getHeader('Content-Length');
115
		if (is_numeric($linkContentLength) && (int) $linkContentLength > 5 * 1024 * 1024) {
116
			$this->logger->debug('Skip resolving links pointing to content length > 5 MB');
117
			return;
118
		}
119
		$linkContentType = $headResponse->getHeader('Content-Type');
120
		$expectedContentType = 'text/html';
121
		$suffixedExpectedContentType = $expectedContentType . ';';
122
		$startsWithSuffixed = substr($linkContentType, 0, strlen($suffixedExpectedContentType)) === $suffixedExpectedContentType;
123
		// check the header begins with the expected content type
124
		if ($linkContentType !== $expectedContentType && !$startsWithSuffixed) {
125
			$this->logger->debug('Skip resolving links pointing to content type that is not "text/html"');
126
			return;
127
		}
128
		try {
129
			$response = $client->get($reference->getId(), [ 'timeout' => 10 ]);
130
		} catch (\Exception $e) {
131
			$this->logger->debug('Failed to fetch link for obtaining open graph data', ['exception' => $e]);
132
			return;
133
		}
134
135
		$responseBody = (string)$response->getBody();
136
137
		// OpenGraph handling
138
		$consumer = new Consumer();
139
		$consumer->useFallbackMode = true;
140
		$object = $consumer->loadHtml($responseBody);
141
142
		$reference->setUrl($reference->getId());
143
144
		if ($object->title) {
145
			$reference->setTitle($object->title);
146
		}
147
148
		if ($object->description) {
149
			$reference->setDescription($object->description);
150
		}
151
152
		if ($object->images) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $object->images of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
153
			try {
154
				$appData = $this->appDataFactory->get('core');
155
				try {
156
					$folder = $appData->getFolder('opengraph');
157
				} catch (NotFoundException $e) {
158
					$folder = $appData->newFolder('opengraph');
159
				}
160
				$response = $client->get($object->images[0]->url, [ 'timeout' => 10 ]);
161
				$contentType = $response->getHeader('Content-Type');
162
				$contentLength = $response->getHeader('Content-Length');
163
164
				if (in_array($contentType, self::ALLOWED_CONTENT_TYPES, true) && $contentLength < self::MAX_PREVIEW_SIZE) {
165
					$stream = Utils::streamFor($response->getBody());
166
					$bodyStream = new LimitStream($stream, self::MAX_PREVIEW_SIZE, 0);
167
					$reference->setImageContentType($contentType);
168
					$folder->newFile(md5($reference->getId()), $bodyStream->getContents());
169
					$reference->setImageUrl($this->urlGenerator->linkToRouteAbsolute('core.Reference.preview', ['referenceId' => md5($reference->getId())]));
170
				}
171
			} catch (GuzzleException $e) {
172
				$this->logger->info('Failed to fetch and store the open graph image for ' . $reference->getId(), ['exception' => $e]);
173
			} catch (\Throwable $e) {
174
				$this->logger->error('Failed to fetch and store the open graph image for ' . $reference->getId(), ['exception' => $e]);
175
			}
176
		}
177
	}
178
179
	public function getCachePrefix(string $referenceId): string {
180
		return $referenceId;
181
	}
182
183
	public function getCacheKey(string $referenceId): ?string {
184
		return null;
185
	}
186
}
187