You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

227 lines
6.1 KiB

  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  7. * @author Georg Ehrke <oc.list@georgehrke.com>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Lukas Reschke <lukas@statuscode.ch>
  10. * @author Morris Jobke <hey@morrisjobke.de>
  11. * @author Roeland Jago Douma <roeland@famdouma.nl>
  12. * @author Steffen Lindner <mail@steffen-lindner.de>
  13. *
  14. * @license GNU AGPL version 3 or any later version
  15. *
  16. * This program is free software: you can redistribute it and/or modify
  17. * it under the terms of the GNU Affero General Public License as
  18. * published by the Free Software Foundation, either version 3 of the
  19. * License, or (at your option) any later version.
  20. *
  21. * This program is distributed in the hope that it will be useful,
  22. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. * GNU Affero General Public License for more details.
  25. *
  26. * You should have received a copy of the GNU Affero General Public License
  27. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  28. *
  29. */
  30. namespace OC\App\AppStore\Fetcher;
  31. use GuzzleHttp\Exception\ConnectException;
  32. use OC\Files\AppData\Factory;
  33. use OCP\AppFramework\Http;
  34. use OCP\AppFramework\Utility\ITimeFactory;
  35. use OCP\Files\IAppData;
  36. use OCP\Files\NotFoundException;
  37. use OCP\Http\Client\IClientService;
  38. use OCP\IConfig;
  39. use OCP\ILogger;
  40. abstract class Fetcher {
  41. public const INVALIDATE_AFTER_SECONDS = 3600;
  42. /** @var IAppData */
  43. protected $appData;
  44. /** @var IClientService */
  45. protected $clientService;
  46. /** @var ITimeFactory */
  47. protected $timeFactory;
  48. /** @var IConfig */
  49. protected $config;
  50. /** @var Ilogger */
  51. protected $logger;
  52. /** @var string */
  53. protected $fileName;
  54. /** @var string */
  55. protected $endpointName;
  56. /** @var string */
  57. protected $version;
  58. /** @var string */
  59. protected $channel;
  60. /**
  61. * @param Factory $appDataFactory
  62. * @param IClientService $clientService
  63. * @param ITimeFactory $timeFactory
  64. * @param IConfig $config
  65. * @param ILogger $logger
  66. */
  67. public function __construct(Factory $appDataFactory,
  68. IClientService $clientService,
  69. ITimeFactory $timeFactory,
  70. IConfig $config,
  71. ILogger $logger) {
  72. $this->appData = $appDataFactory->get('appstore');
  73. $this->clientService = $clientService;
  74. $this->timeFactory = $timeFactory;
  75. $this->config = $config;
  76. $this->logger = $logger;
  77. }
  78. /**
  79. * Fetches the response from the server
  80. *
  81. * @param string $ETag
  82. * @param string $content
  83. *
  84. * @return array
  85. */
  86. protected function fetch($ETag, $content) {
  87. $appstoreenabled = $this->config->getSystemValue('appstoreenabled', true);
  88. if (!$appstoreenabled) {
  89. return [];
  90. }
  91. $options = [
  92. 'timeout' => 60,
  93. ];
  94. if ($ETag !== '') {
  95. $options['headers'] = [
  96. 'If-None-Match' => $ETag,
  97. ];
  98. }
  99. $client = $this->clientService->newClient();
  100. $response = $client->get($this->getEndpoint(), $options);
  101. $responseJson = [];
  102. if ($response->getStatusCode() === Http::STATUS_NOT_MODIFIED) {
  103. $responseJson['data'] = json_decode($content, true);
  104. } else {
  105. $responseJson['data'] = json_decode($response->getBody(), true);
  106. $ETag = $response->getHeader('ETag');
  107. }
  108. $responseJson['timestamp'] = $this->timeFactory->getTime();
  109. $responseJson['ncversion'] = $this->getVersion();
  110. if ($ETag !== '') {
  111. $responseJson['ETag'] = $ETag;
  112. }
  113. return $responseJson;
  114. }
  115. /**
  116. * Returns the array with the categories on the appstore server
  117. *
  118. * @return array
  119. */
  120. public function get() {
  121. $appstoreenabled = $this->config->getSystemValue('appstoreenabled', true);
  122. $internetavailable = $this->config->getSystemValue('has_internet_connection', true);
  123. if (!$appstoreenabled || !$internetavailable) {
  124. return [];
  125. }
  126. $rootFolder = $this->appData->getFolder('/');
  127. $ETag = '';
  128. $content = '';
  129. try {
  130. // File does already exists
  131. $file = $rootFolder->getFile($this->fileName);
  132. $jsonBlob = json_decode($file->getContent(), true);
  133. if (is_array($jsonBlob)) {
  134. // No caching when the version has been updated
  135. if (isset($jsonBlob['ncversion']) && $jsonBlob['ncversion'] === $this->getVersion()) {
  136. // If the timestamp is older than 3600 seconds request the files new
  137. if ((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - self::INVALIDATE_AFTER_SECONDS)) {
  138. return $jsonBlob['data'];
  139. }
  140. if (isset($jsonBlob['ETag'])) {
  141. $ETag = $jsonBlob['ETag'];
  142. $content = json_encode($jsonBlob['data']);
  143. }
  144. }
  145. }
  146. } catch (NotFoundException $e) {
  147. // File does not already exists
  148. $file = $rootFolder->newFile($this->fileName);
  149. }
  150. // Refresh the file content
  151. try {
  152. $responseJson = $this->fetch($ETag, $content);
  153. $file->putContent(json_encode($responseJson));
  154. return json_decode($file->getContent(), true)['data'];
  155. } catch (ConnectException $e) {
  156. $this->logger->warning('Could not connect to appstore: ' . $e->getMessage(), ['app' => 'appstoreFetcher']);
  157. return [];
  158. } catch (\Exception $e) {
  159. $this->logger->logException($e, ['app' => 'appstoreFetcher', 'level' => ILogger::WARN]);
  160. return [];
  161. }
  162. }
  163. /**
  164. * Get the currently Nextcloud version
  165. * @return string
  166. */
  167. protected function getVersion() {
  168. if ($this->version === null) {
  169. $this->version = $this->config->getSystemValue('version', '0.0.0');
  170. }
  171. return $this->version;
  172. }
  173. /**
  174. * Set the current Nextcloud version
  175. * @param string $version
  176. */
  177. public function setVersion(string $version) {
  178. $this->version = $version;
  179. }
  180. /**
  181. * Get the currently Nextcloud update channel
  182. * @return string
  183. */
  184. protected function getChannel() {
  185. if ($this->channel === null) {
  186. $this->channel = \OC_Util::getChannel();
  187. }
  188. return $this->channel;
  189. }
  190. /**
  191. * Set the current Nextcloud update channel
  192. * @param string $channel
  193. */
  194. public function setChannel(string $channel) {
  195. $this->channel = $channel;
  196. }
  197. protected function getEndpoint(): string {
  198. return $this->config->getSystemValue('appstoreurl', 'https://apps.nextcloud.com/api/v1') . '/' . $this->endpointName;
  199. }
  200. }