Http::createPremise()   D
last analyzed

Complexity

Conditions 13
Paths 6

Size

Total Lines 104
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 13

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 104
ccs 46
cts 46
cp 1
rs 4.9922
cc 13
eloc 63
nc 6
nop 1
crap 13

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Tartana\Host\Common;
3
4
use GuzzleHttp\Client;
5
use GuzzleHttp\ClientInterface;
6
use GuzzleHttp\Cookie\FileCookieJar;
7
use GuzzleHttp\Exception\RequestException;
8
use GuzzleHttp\Psr7\Request;
9
use GuzzleHttp\Psr7\Response;
10
use GuzzleHttp\RequestOptions;
11
use Joomla\Registry\Registry;
12
use League\Flysystem\Adapter\Local;
13
use League\Flysystem\Config;
14
use Tartana\Domain\Command\SaveDownloads;
15
use Tartana\Entity\Download;
16
use Tartana\Host\HostInterface;
17
use Tartana\Mixins\CommandBusAwareTrait;
18
use Tartana\Mixins\LoggerAwareTrait;
19
20
class Http implements HostInterface
21
{
22
	use LoggerAwareTrait;
23
	use CommandBusAwareTrait;
24
25
	private $configuration = null;
26
27
	private $client = null;
28
29 77
	public function __construct(Registry $configuration, ClientInterface $client = null)
30
	{
31 77
		$this->configuration = $configuration;
32 77
		$this->setClient($client);
33 77
	}
34
35 3
	public function fetchLinkList($link)
36
	{
37
		return [
38 3
			$link
39
		];
40
	}
41
42 9
	public function fetchDownloadInfo(array $downloads)
43
	{
44 9
		foreach ($downloads as $download) {
45
			// Connection check
46
			try {
47 9
				$originalName = $this->parseFileName($this->getClient()
48 9
					->head($download->getLink()));
49 7
				if (empty($originalName)) {
50 4
					$originalName = basename($download->getLink());
51
				}
52 7
				if (!empty($originalName) && empty($download->getFileName())) {
53 7
					$download->setFileName($originalName);
54
				}
55 2
			} catch (\Exception $e) {
56 2
				$this->log('Exception fetching head for connection test: ' . $e->getMessage());
57 2
				$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_URL');
58 9
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
59
			}
60
		}
61 9
	}
62
63 46
	public function download(array $downloads)
64
	{
65 46
		if (empty($downloads)) {
66 2
			return [];
67
		}
68
69
		try {
70 44
			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...
71 5
				foreach ($downloads as $download) {
72 5
					$download->setState(Download::STATE_DOWNLOADING_ERROR);
73 5
					$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_LOGIN');
74
				}
75 5
				$this->handleCommand(new SaveDownloads($downloads));
76 42
				return [];
77
			}
78 2
		} catch (\Exception $e) {
79 2
			foreach ($downloads as $download) {
80 2
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
81 2
				$download->setMessage($e->getMessage());
82
			}
83 2
			$this->handleCommand(new SaveDownloads($downloads));
84 2
			return [];
85
		}
86
87 37
		$promises = [];
88 37
		foreach ($downloads as $download) {
89 37
			$download->setStartedAt(new \DateTime());
90
			try {
91 37
				$promises[] = $this->createPremise($download);
92 11
			} catch (\Exception $e) {
93 11
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
94 11
				$download->setMessage($e->getMessage());
95 11
				$download->setFinishedAt(new \DateTime());
96 11
				$this->handleCommand(new SaveDownloads([
97 11
					$download
98
				]));
99 37
				continue;
100
			}
101
		}
102
103 37
		return $promises;
104
	}
105
106
	/**
107
	 * If none is internaly configured a new instance will be created.
108
	 *
109
	 * @return \GuzzleHttp\ClientInterface
110
	 */
111 60
	public function getClient()
112
	{
113 60
		if (!$this->client) {
114 2
			$fs   = new Local(TARTANA_PATH_ROOT . '/var/tmp/');
115 2
			$name = strtolower((new \ReflectionClass($this))->getShortName()) . '.cookie';
116 2
			if (!$fs->has($name) || $this->getConfiguration()->get('clearSession', false)) {
117 2
				$fs->write($name, '', new Config());
118
			}
119 2
			$this->client = new Client([
120 2
				'cookies' => new FileCookieJar($fs->applyPathPrefix($name), true)
121
			]);
122
		}
123 60
		return $this->client;
124
	}
125
126 77
	public function setClient(ClientInterface $client = null)
127
	{
128 77
		$this->client = $client;
129 77
	}
130
131
	/**
132
	 * Returns the configuration.
133
	 *
134
	 * @return \Joomla\Registry\Registry
135
	 */
136 35
	protected function getConfiguration()
137
	{
138 35
		return $this->configuration;
139
	}
140
141
	/**
142
	 * Returns the real url to download, subclasses can do here some
143
	 * preprocessing of the given download.
144
	 * The download will be saved after that operation. If null is returned, the
145
	 * download will not be performed.
146
	 *
147
	 * @param Download $download
148
	 * @return string
149
	 */
150 18
	protected function getUrlToDownload(Download $download)
151
	{
152 18
		return $download->getLink();
153
	}
154
155
	/**
156
	 * Login function which can be used on subclasses to authenticate before the
157
	 * download is done.
158
	 *
159
	 * @return boolean
160
	 */
161 23
	protected function login()
162
	{
163 23
		return true;
164
	}
165
166
	/**
167
	 * Returns if the local client has a cookie with the given name and is not
168
	 * expired.
169
	 *
170
	 * @param string $name
171
	 * @return \GuzzleHttp\Cookie\SetCookie
172
	 */
173 21
	protected function getCookie($name)
174
	{
175 21
		$cookies = $this->getClient()->getConfig('cookies');
176
177 21
		if (!$cookies instanceof \Traversable) {
178 11
			return null;
179
		}
180
181 10
		foreach ($cookies as $cookie) {
182
			/** @var \GuzzleHttp\Cookie\SetCookie $cookie */
183 10
			if ($cookie->getName() != $name) {
184 2
				continue;
185
			}
186 8
			if (!$cookie->getExpires() || $cookie->getExpires() > time()) {
187 8
				return $cookie;
188
			}
189
		}
190
191 4
		return null;
192
	}
193
194
	/**
195
	 * Subclasses can define here the headers before the file is downloaded.
196
	 * It must return an array of headers.
197
	 *
198
	 * @param Download $download
199
	 * @return array
200
	 */
201 25
	protected function getHeadersForDownload(Download $download)
202
	{
203 25
		return [];
204
	}
205
206
	/**
207
	 * Parses the file name from a response.
208
	 * Mainly it tryes to analyze the headers.
209
	 *
210
	 * @param Response $response
211
	 * @return string|NULL
212
	 */
213 30
	protected function parseFileName(Response $response)
214
	{
215 30
		$dispHeader = $response->getHeader('Content-Disposition');
216 30
		if ($dispHeader && preg_match('/.*filename=([^ ]+)/', $dispHeader[0], $matches)) {
217 23
			return trim($matches[1], '";');
218
		}
219 7
		return null;
220
	}
221
222 37
	private function createPremise(Download $download)
223
	{
224 37
		$url = $this->getUrlToDownload($download);
225 36
		if (!$url) {
226 8
			if (!$download->getMessage()) {
227 8
				$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_FAILED_REAL_URL');
228
			}
229 8
			throw new \Exception($download->getMessage());
230
		}
231
232 28
		$tmpFileName = 'tmp-' . $download->getId() . '.bin';
233
234 28
		$me = $this;
235 28
		$fs = new Local($download->getDestination());
236
237 26
		$this->log('Downloading download ' . $download->getId() . ' to ' . $fs->applyPathPrefix($tmpFileName));
238
239
		// @codeCoverageIgnoreStart
240
		$options = [
241
			RequestOptions::SINK => $fs->applyPathPrefix($tmpFileName),
242
			RequestOptions::PROGRESS => function ($totalSize, $downloadedSize) use ($download, $me) {
243
				if (!$downloadedSize || !$totalSize) {
244
					return;
245
				}
246
				$progress = (100 / $totalSize) * $downloadedSize;
247
248
				if ($progress < $download->getProgress() + (rand(100, 700) / 1000)) {
249
					// Reducing write transactions on the
250
					// repository
251
					return;
252
				}
253
254
				$download->setProgress($progress);
255
				$download->setSize($totalSize);
256
				$me->handleCommand(new SaveDownloads([
257
					$download
258
				]));
259
			}
260
		];
261
		if (defined('CURLOPT_TCP_KEEPALIVE')) {
262
			// Needed to keep the session alive on slow downloads
263
			$options['curl'] = [
264
				CURLOPT_TCP_KEEPALIVE => 1,
265
				CURLOPT_TCP_KEEPIDLE => 30,
266
				CURLOPT_TCP_KEEPINTVL => 15
267
			];
268
		}
269
		// @codeCoverageIgnoreEnd
270
271 26
		if ($this->getConfiguration()->get('speedlimit') > 0) {
272 2
			$options['curl'][CURLOPT_MAX_RECV_SPEED_LARGE] = $this->getConfiguration()->get('speedlimit') * 1000;
273
		}
274
275 26
		$options[RequestOptions::HEADERS] = $this->getHeadersForDownload($download);
276
277 26
		$request = new Request('get', $url);
278 26
		$promise = $this->getClient()->sendAsync($request, $options);
279
280 26
		$promise->then(
281
			function (Response $resp) use ($fs, $tmpFileName, $download, $me) {
282 24
				$originalFileName = $this->parseFileName($resp);
283 24
				if (empty($download->getFileName()) && !empty($originalFileName)) {
284 19
					$download->setFileName($originalFileName);
285
				}
286
287 24
				if (!empty($download->getFileName())) {
288 22
					$fs->rename($tmpFileName, $download->getFileName());
289
				} else {
290 2
					$download->setFileName($tmpFileName);
291
				}
292
293
				// Hash check
294 24
				if (!empty($download->getHash())) {
295 6
					$hash = md5_file($fs->applyPathPrefix($download->getFileName()));
296 6
					if ($download->getHash() != $hash) {
297 2
						$fs->delete($download->getFileName());
298 2
						$download->setState(Download::STATE_DOWNLOADING_ERROR);
299 2
						$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_HASH');
300 2
						$download->setFinishedAt(new \DateTime());
301 2
						$me->handleCommand(new SaveDownloads([
302 2
							$download
303
						]));
304 2
						return;
305
					}
306
				}
307
308 22
				$download->setState(Download::STATE_DOWNLOADING_COMPLETED);
309 22
				$download->setProgress(100);
310 22
				$download->setFinishedAt(new \DateTime());
311 22
				$me->handleCommand(new SaveDownloads([
312 22
					$download
313
				]));
314 26
			},
315 26
			function (RequestException $e) use ($download, $me) {
316 2
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
317 2
				$download->setMessage($e->getMessage());
318 2
				$download->setFinishedAt(new \DateTime());
319 2
				$me->handleCommand(new SaveDownloads([
320 2
					$download
321
				]));
322 26
			}
323
		);
324 26
		return $promise;
325
	}
326
}
327