1<?php
2/**
3 * @author Victor Dubiniuk <dubiniuk@owncloud.com>
4 *
5 * @copyright Copyright (c) 2015, ownCloud, Inc.
6 * @license AGPL-3.0
7 *
8 * This code is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Affero General Public License, version 3,
10 * as published by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License, version 3,
18 * along with this program.  If not, see <http://www.gnu.org/licenses/>
19 *
20 */
21
22namespace Owncloud\Updater\Utils;
23
24use GuzzleHttp\Client;
25
26/**
27 * Class Fetcher
28 *
29 * @package Owncloud\Updater\Utils
30 */
31class Fetcher {
32
33	const DEFAULT_BASE_URL = 'https://updates.owncloud.com/server/';
34
35	/**
36	 * @var Locator $locator
37	 */
38	protected $locator;
39
40	/**
41	 * @var ConfigReader $configReader
42	 */
43	protected $configReader;
44
45	/**
46	 * @var Client $httpClient
47	 */
48	protected $httpClient;
49	protected $requiredFeedEntries = [
50		'version',
51		'versionstring',
52		'url'
53	];
54
55	/**
56	 * Constructor
57	 *
58	 * @param Client $httpClient
59	 * @param Locator $locator
60	 * @param ConfigReader $configReader
61	 */
62	public function __construct(Client $httpClient, Locator $locator, ConfigReader $configReader){
63		$this->httpClient = $httpClient;
64		$this->locator = $locator;
65		$this->configReader = $configReader;
66	}
67
68	/**
69	 * Download new ownCloud package
70	 * @param Feed $feed
71	 * @param Callable $onProgress
72	 * @throws \Exception
73	 * @throws \UnexpectedValueException
74	 */
75	public function getOwncloud(Feed $feed, callable $onProgress){
76		if ($feed->isValid()){
77			$downloadPath = $this->getBaseDownloadPath($feed);
78			if (!is_writable(dirname($downloadPath))){
79				throw new \Exception(dirname($downloadPath) . ' is not writable.');
80			}
81			$url = $feed->getUrl();
82			$request = $this->httpClient->createRequest(
83					'GET',
84					$url,
85					[
86						'save_to' => $downloadPath,
87						'timeout' => 600
88					]
89			);
90			$request->getEmitter()->on('progress', $onProgress);
91			$response = $this->httpClient->send($request);
92			$this->validateResponse($response);
93		}
94	}
95
96	/**
97	 * Produce a local path to save the package to
98	 * @param Feed $feed
99	 * @return string
100	 */
101	public function getBaseDownloadPath(Feed $feed){
102		$basePath = $this->locator->getDownloadBaseDir();
103		return $basePath . '/' . $feed->getDownloadedFileName();
104	}
105
106	/**
107	 * Get md5 sum for the package
108	 * @param Feed $feed
109	 * @return string
110	 */
111	public function getMd5(Feed $feed){
112		$fullChecksum = $this->download($feed->getChecksumUrl());
113		// we got smth like "5776cbd0a95637ade4b2c0d8694d8fca  -"
114		//strip trailing space & dash
115		return substr($fullChecksum, 0, 32);
116	}
117
118	/**
119	 * Read update feed for new releases
120	 * @return Feed
121	 */
122	public function getFeed(){
123		$url = $this->getFeedUrl();
124		$xml = $this->download($url);
125		$tmp = [];
126		if ($xml){
127			$loadEntities = libxml_disable_entity_loader(true);
128			$data = @simplexml_load_string($xml);
129			libxml_disable_entity_loader($loadEntities);
130			if ($data !== false){
131				$tmp['version'] = (string) $data->version;
132				$tmp['versionstring'] = (string) $data->versionstring;
133				$tmp['url'] = (string) $data->url;
134				$tmp['web'] = (string) $data->web;
135			}
136		}
137
138		return new Feed($tmp);
139	}
140
141	/**
142	 * @return mixed|string
143	 */
144	public function getUpdateChannel(){
145		$channel = $this->configReader->getByPath('apps.core.OC_Channel');
146		if (is_null($channel)) {
147			return $this->locator->getChannelFromVersionsFile();
148		}
149
150		return $channel;
151	}
152
153	/**
154	 * Produce complete feed URL
155	 * @return string
156	 */
157	protected function getFeedUrl(){
158		$currentVersion = $this->configReader->getByPath('system.version');
159		$version = explode('.', $currentVersion);
160		$version['installed'] = $this->configReader->getByPath('apps.core.installedat');
161		$version['updated'] = $this->configReader->getByPath('apps.core.lastupdatedat');
162		$version['updatechannel'] = $this->getUpdateChannel();
163		$version['edition'] = $this->configReader->getEdition();
164		$version['build'] = $this->locator->getBuild();
165
166		// Read updater server URL from config
167		$updaterServerUrl = $this->configReader->get(['system', 'updater.server.url']);
168		if ((bool) $updaterServerUrl === false){
169			$updaterServerUrl = self::DEFAULT_BASE_URL;
170		}
171
172		$url = $updaterServerUrl . '?version=' . implode('x', $version);
173		return $url;
174	}
175
176	/**
177	 * Get URL content
178	 * @param string $url
179	 * @return string
180	 * @throws \UnexpectedValueException
181	 */
182	protected function download($url){
183		$response = $this->httpClient->get($url, ['timeout' => 600]);
184		$this->validateResponse($response);
185		return $response->getBody()->getContents();
186	}
187
188	/**
189	 * Check if request was successful
190	 * @param \GuzzleHttp\Message\ResponseInterface $response
191	 * @throws \UnexpectedValueException
192	 */
193	protected function validateResponse($response){
194		if ($response->getStatusCode() !== 200){
195			throw new \UnexpectedValueException(
196					'Failed to download '
197					. $response->getEffectiveUrl()
198					. '. Server responded with '
199					. $response->getStatusCode()
200					. ' instead of 200.');
201		}
202	}
203
204}
205