Completed
Push — master ( 3a9f70...45da6a )
by C
05:33
created

Http::fetchDownloadInfo()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
ccs 11
cts 11
cp 1
rs 8.6737
cc 5
eloc 11
nc 6
nop 1
crap 5
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 53
	public function __construct (Registry $configuration, ClientInterface $client = null)
29
	{
30 53
		$this->configuration = $configuration;
31 53
		$this->setClient($client);
32 53
	}
33
34 7
	public function fetchDownloadInfo (array $downloads)
35
	{
36 7
		foreach ($downloads as $download)
37
		{
38
			// Connection check
39
			try
40
			{
41 7
				$originalName = $this->parseFileName($this->getClient()
42 7
					->head($download->getLink()));
43 5
				if (! empty($originalName) && empty($download->getFileName()))
44
				{
45 5
					$download->setFileName($originalName);
46
				}
47
			}
48 2
			catch (\Exception $e)
49
			{
50 2
				$this->log('Exception fetching head for connection test: ' . $e->getMessage());
51 2
				$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_URL');
52 7
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
53
			}
54
		}
55 7
	}
56
57 37
	public function download (array $downloads)
58
	{
59 37
		if (empty($downloads))
60
		{
61 2
			return [];
62
		}
63
64
		try
65
		{
66 35 View Code Duplication
			if (! $this->login())
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
67
			{
68 5
				foreach ($downloads as $download)
69
				{
70 5
					$download->setState(Download::STATE_DOWNLOADING_ERROR);
71 5
					$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_LOGIN');
72
				}
73 5
				$this->handleCommand(new SaveDownloads($downloads));
74 33
				return [];
75
			}
76
		}
77 2
		catch (\Exception $e)
78
		{
79 2
			foreach ($downloads as $download)
80
			{
81 2
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
82 2
				$download->setMessage($e->getMessage());
83
			}
84 2
			$this->handleCommand(new SaveDownloads($downloads));
85 2
			return [];
86
		}
87
88 28
		$promises = [];
89 28
		foreach ($downloads as $download)
90
		{
91
			try
92
			{
93 28
				$url = $this->getUrlToDownload($download);
94 27
				if (! $url)
95
				{
96 5
					if (! $download->getMessage())
97
					{
98 2
						$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_URL');
99
					}
100 5
					throw new \Exception($download->getMessage());
101
				}
102
103 22
				$tmpFileName = 'tmp-' . $download->getId() . '.bin';
104
105 22
				$me = $this;
106 22
				$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 20
				$options[RequestOptions::HEADERS] = $this->getHeadersForDownload($download);
135
136 20
				$request = new Request('get', $url);
137 20
				$promise = $this->getClient()->sendAsync($request, $options);
138
139 20
				$promise->then(
140
						function  (Response $resp) use ( $fs, $tmpFileName, $download, $me) {
141 18
							$originalFileName = $this->parseFileName($resp);
142 18
							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 14
								$download->setFileName($originalFileName);
145
							}
146
147 18
							if (! empty($download->getFileName()))
148
							{
149 16
								$fs->rename($tmpFileName, $download->getFileName());
150
							}
151
							else
152
							{
153 2
								$download->setFileName($tmpFileName);
154
							}
155 18
							$download->setState(Download::STATE_DOWNLOADING_COMPLETED);
156 18
							$download->setProgress(100);
157 18
							$download->setFinishedAt(new \DateTime());
158 18
							$me->handleCommand(new SaveDownloads([
159 18
									$download
160
							]));
161 20
						},
162 20
						function  (RequestException $e) use ( $download, $me) {
163 2
							$download->setState(Download::STATE_DOWNLOADING_ERROR);
164 2
							$download->setMessage($e->getMessage());
165 2
							$download->setFinishedAt(new \DateTime());
166 2
							$me->handleCommand(new SaveDownloads([
167 2
									$download
168
							]));
169 20
						});
170 20
				$promises[] = $promise;
171
			}
172 8
			catch (\Exception $e)
173
			{
174 8
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
175 8
				$download->setMessage($e->getMessage());
176 8
				$download->setFinishedAt(new \DateTime());
177 8
				$this->handleCommand(new SaveDownloads([
178 8
						$download
179
				]));
180 28
				continue;
181
			}
182
		}
183
184 28
		return $promises;
185
	}
186
187
	/**
188
	 * If none is internaly configured a new instance will be created.
189
	 *
190
	 * @return \GuzzleHttp\ClientInterface
191
	 */
192 40
	public function getClient ()
193
	{
194 40
		if (! $this->client)
195
		{
196 2
			$fs = new Local(TARTANA_PATH_ROOT . '/var/tmp/');
197 2
			$name = strtolower((new \ReflectionClass($this))->getShortName()) . '.cookie';
198 2
			if (! $fs->has($name) || $this->getConfiguration()->get('clearSession', false))
199
			{
200 2
				$fs->write($name, '', new Config());
201
			}
202 2
			$this->client = new Client([
203 2
					'cookies' => new FileCookieJar($fs->applyPathPrefix($name), true)
204
			]);
205
		}
206 40
		return $this->client;
207
	}
208
209 53
	public function setClient (ClientInterface $client = null)
210
	{
211 53
		$this->client = $client;
212 53
	}
213
214
	/**
215
	 * Returns the configuration.
216
	 *
217
	 * @return \Joomla\Registry\Registry
218
	 */
219 16
	protected function getConfiguration ()
220
	{
221 16
		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 14
	protected function getUrlToDownload (Download $download)
234
	{
235 14
		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 16
	protected function login ()
245
	{
246 16
		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 15
	protected function hasCookie ($name)
257
	{
258 15
		foreach ($this->getClient()->getConfig('cookies') as $cookie)
259
		{
260
			/** @var \GuzzleHttp\Cookie\SetCookie $cookie */
261 6
			if ($cookie->getName() != $name)
262
			{
263 2
				continue;
264
			}
265 4
			if ($cookie->getExpires() > time())
266
			{
267 4
				return true;
268
			}
269
		}
270
271 13
		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 19
	protected function getHeadersForDownload (Download $download)
282
	{
283 19
		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 22
	protected function parseFileName (Response $response)
294
	{
295 22
		$dispHeader = $response->getHeader('Content-Disposition');
296 22
		if ($dispHeader && preg_match('/.*filename=([^ ]+)/', $dispHeader[0], $matches))
297
		{
298 18
			return trim($matches[1], '";');
299
		}
300 4
		return null;
301
	}
302
}
303