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