Completed
Push — master ( 1e5574...2c7872 )
by C
06:04
created

Http::fetchDownloadInfo()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
rs 8.6737
cc 5
eloc 11
nc 6
nop 1
1
<?php
2
namespace Tartana\Host\Common;
3
use GuzzleHttp\Client;
4
use GuzzleHttp\ClientInterface;
5
use GuzzleHttp\Cookie\FileCookieJar;
6
use GuzzleHttp\Exception\RequestException;
7
use GuzzleHttp\Psr7\Request;
8
use GuzzleHttp\Psr7\Response;
9
use GuzzleHttp\RequestOptions;
10
use Joomla\Registry\Registry;
11
use League\Flysystem\Adapter\Local;
12
use League\Flysystem\Config;
13
use Tartana\Domain\Command\SaveDownloads;
14
use Tartana\Entity\Download;
15
use Tartana\Host\HostInterface;
16
use Tartana\Mixins\CommandBusAwareTrait;
17
use Tartana\Mixins\LoggerAwareTrait;
18
19
class Http implements HostInterface
20
{
21
	use LoggerAwareTrait;
22
	use CommandBusAwareTrait;
23
24
	private $configuration = null;
25
26
	private $client = null;
27
28
	public function __construct (Registry $configuration, ClientInterface $client = null)
29
	{
30
		$this->configuration = $configuration;
31
		$this->setClient($client);
32
	}
33
34
	public function fetchDownloadInfo (array $downloads)
35
	{
36
		foreach ($downloads as $download)
37
		{
38
			// Connection check
39
			try
40
			{
41
				$originalName = $this->parseFileName($this->getClient()
42
					->head($url));
0 ignored issues
show
Bug introduced by
The variable $url does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
43
				if (! empty($originalName) && empty($download->getFileName()))
44
				{
45
					$download->setFileName($originalName);
46
				}
47
			}
48
			catch (\Exception $e)
49
			{
50
				$this->log('Exception fetching head for connection test: ' . $e->getMessage());
51
				$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_URL');
52
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
53
			}
54
		}
55
	}
56
57
	public function download (array $downloads)
58
	{
59
		if (empty($downloads))
60
		{
61
			return [];
62
		}
63
64
		try
65
		{
66
			if (! $this->login())
67
			{
68
				foreach ($downloads as $download)
69
				{
70
					$download->setState(Download::STATE_DOWNLOADING_ERROR);
71
					$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_LOGIN');
72
				}
73
				$this->handleCommand(new SaveDownloads($downloads));
74
				return [];
75
			}
76
		}
77
		catch (\Exception $e)
78
		{
79
			foreach ($downloads as $download)
80
			{
81
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
82
				$download->setMessage($e->getMessage());
83
			}
84
			$this->handleCommand(new SaveDownloads($downloads));
85
			return [];
86
		}
87
88
		$promises = [];
89
		foreach ($downloads as $download)
90
		{
91
			try
92
			{
93
				$url = $this->getUrlToDownload($download);
94
				if (! $url)
95
				{
96
					if (! $download->getMessage())
97
					{
98
						$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_URL');
99
					}
100
					throw new \Exception($download->getMessage());
101
				}
102
103
				$tmpFileName = 'tmp-' . $download->getId() . '.bin';
104
105
				$me = $this;
106
				$fs = new Local($download->getDestination());
107
108
				// @codeCoverageIgnoreStart
109
				$options = [
110
						RequestOptions::SINK => $fs->applyPathPrefix($tmpFileName),
111
						RequestOptions::PROGRESS => function  ($totalSize, $downloadedSize) use ( $download, $me) {
112
							if (! $downloadedSize || ! $totalSize)
113
							{
114
								return;
115
							}
116
							$progress = (100 / $totalSize) * $downloadedSize;
117
118
							if ($progress < $download->getProgress() + (rand(100, 700) / 1000))
119
							{
120
								// Reducing write transactions on the
121
								// repository
122
								return;
123
							}
124
125
							$download->setProgress($progress);
126
							$download->setSize($totalSize);
127
							$me->handleCommand(new SaveDownloads([
128
									$download
129
							]));
130
						}
131
				];
132
				// @codeCoverageIgnoreEnd
133
134
				$options[RequestOptions::HEADERS] = $this->getHeadersForDownload($download);
135
136
				$request = new Request('get', $url);
137
				$promise = $this->getClient()->sendAsync($request, $options);
138
139
				$promise->then(
140
						function  (Response $resp) use ( $fs, $tmpFileName, $download, $me) {
141
							$originalFileName = $this->parseFileName($resp);
142
							if (empty($download->getFileName()) && $originalFileName)
0 ignored issues
show
Bug Best Practice introduced by
The expression $originalFileName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
143
							{
144
								$download->setFileName($originalFileName);
145
							}
146
147
							if (! empty($download->getFileName()))
148
							{
149
								$fs->rename($tmpFileName, $download->getFileName());
150
							}
151
							else
152
							{
153
								$download->setFileName($tmpFileName);
154
							}
155
							$download->setState(Download::STATE_DOWNLOADING_COMPLETED);
156
							$download->setProgress(100);
157
							$download->setFinishedAt(new \DateTime());
158
							$me->handleCommand(new SaveDownloads([
159
									$download
160
							]));
161
						},
162
						function  (RequestException $e) use ( $download, $me) {
163
							$download->setState(Download::STATE_DOWNLOADING_ERROR);
164
							$download->setMessage($e->getMessage());
165
							$download->setFinishedAt(new \DateTime());
166
							$me->handleCommand(new SaveDownloads([
167
									$download
168
							]));
169
						});
170
				$promises[] = $promise;
171
			}
172
			catch (\Exception $e)
173
			{
174
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
175
				$download->setMessage($e->getMessage());
176
				$download->setFinishedAt(new \DateTime());
177
				$this->handleCommand(new SaveDownloads([
178
						$download
179
				]));
180
				continue;
181
			}
182
		}
183
184
		return $promises;
185
	}
186
187
	/**
188
	 * If none is internaly configured a new instance will be created.
189
	 *
190
	 * @return \GuzzleHttp\ClientInterface
191
	 */
192
	public function getClient ()
193
	{
194
		if (! $this->client)
195
		{
196
			$fs = new Local(TARTANA_PATH_ROOT . '/var/tmp/');
197
			$name = strtolower((new \ReflectionClass($this))->getShortName()) . '.cookie';
198
			if (! $fs->has($name) || $this->getConfiguration()->get('clearSession', false))
199
			{
200
				$fs->write($name, '', new Config());
201
			}
202
			$this->client = new Client([
203
					'cookies' => new FileCookieJar($fs->applyPathPrefix($name), true)
204
			]);
205
		}
206
		return $this->client;
207
	}
208
209
	public function setClient (ClientInterface $client = null)
210
	{
211
		$this->client = $client;
212
	}
213
214
	/**
215
	 * Returns the configuration.
216
	 *
217
	 * @return \Joomla\Registry\Registry
218
	 */
219
	protected function getConfiguration ()
220
	{
221
		return $this->configuration;
222
	}
223
224
	/**
225
	 * Returns the real url to download, subclasses can do here some
226
	 * preprocessing of the given download.
227
	 * The download will be saved after that operation. If null is returned, the
228
	 * download will not be performed.
229
	 *
230
	 * @param Download $download
231
	 * @return string
232
	 */
233
	protected function getUrlToDownload (Download $download)
234
	{
235
		return $download->getLink();
236
	}
237
238
	/**
239
	 * Login function which can be used on subclasses to authenticate before the
240
	 * download is done.
241
	 *
242
	 * @return boolean
243
	 */
244
	protected function login ()
245
	{
246
		return true;
247
	}
248
249
	/**
250
	 * Returns if the local client has a cookie with the given name and is not
251
	 * expired.
252
	 *
253
	 * @param string $name
254
	 * @return boolean
255
	 */
256
	protected function hasCookie ($name)
257
	{
258
		foreach ($this->getClient()->getConfig('cookies') as $cookie)
259
		{
260
			/** @var \GuzzleHttp\Cookie\SetCookie $cookie */
261
			if ($cookie->getName() != $name)
262
			{
263
				continue;
264
			}
265
			if ($cookie->getExpires() > time())
266
			{
267
				return true;
268
			}
269
		}
270
271
		return false;
272
	}
273
274
	/**
275
	 * Subclasses can define here the headers before the file is downloaded.
276
	 * It must return an array of headers.
277
	 *
278
	 * @param Download $download
279
	 * @return array
280
	 */
281
	protected function getHeadersForDownload (Download $download)
282
	{
283
		return [];
284
	}
285
286
	/**
287
	 * Parses the file name from a response.
288
	 * Mainly it tryes to analyze the headers.
289
	 * * @param Response $response
290
	 *
291
	 * @return string|NULL
292
	 */
293
	protected function parseFileName (Response $response)
294
	{
295
		$dispHeader = $response->getHeader('Content-Disposition');
296
		if ($dispHeader && preg_match('/.*filename=([^ ]+)/', $dispHeader[0], $matches))
297
		{
298
			return trim($matches[1], '";');
299
		}
300
		return null;
301
	}
302
}
303