CrmUi - Fix crmSelect2 to work with ngOptions
[civicrm-core.git] / CRM / Extension / Downloader.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 * This class handles downloads of remotely-provided extensions
14 *
15 * @package CRM
ca5cec67 16 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
17 */
18class CRM_Extension_Downloader {
7865cc4e
MW
19
20 /**
21 * @var GuzzleHttp\Client
22 */
23 protected $guzzleClient;
24
25 /**
26 * @return \GuzzleHttp\Client
27 */
28 public function getGuzzleClient(): \GuzzleHttp\Client {
29 return $this->guzzleClient ?? new \GuzzleHttp\Client();
30 }
31
32 /**
33 * @param \GuzzleHttp\Client $guzzleClient
34 */
35 public function setGuzzleClient(\GuzzleHttp\Client $guzzleClient) {
36 $this->guzzleClient = $guzzleClient;
37 }
38
6a488035 39 /**
51dda21e
SL
40 * @var CRM_Extension_Container_Basic
41 * The place where downloaded extensions are ultimately stored
6a488035
TO
42 */
43 public $container;
44
45 /**
51dda21e
SL
46 * @var string
47 * Local path to a temporary data directory
6a488035
TO
48 */
49 public $tmpDir;
50
51 /**
fd31fa4c 52 * @param CRM_Extension_Manager $manager
f41911fd
TO
53 * @param string $containerDir
54 * The place to store downloaded & extracted extensions.
6a488035
TO
55 * @param string $tmpDir
56 */
57 public function __construct(CRM_Extension_Manager $manager, $containerDir, $tmpDir) {
58 $this->manager = $manager;
59 $this->containerDir = $containerDir;
60 $this->tmpDir = $tmpDir;
61 }
62
63 /**
fe482240 64 * Determine whether downloading is supported.
6a488035 65 *
19ec0aa5
MWMC
66 * @param \CRM_EXtension_Info $extensionInfo Optional info for (updated) extension
67 *
a6c01b45
CW
68 * @return array
69 * list of error messages; empty if OK
6a488035 70 */
19ec0aa5 71 public function checkRequirements($extensionInfo = NULL) {
affcc9d2 72 $errors = [];
6a488035 73
22e263ad 74 if (!$this->containerDir || !is_dir($this->containerDir) || !is_writable($this->containerDir)) {
6a488035
TO
75 $civicrmDestination = urlencode(CRM_Utils_System::url('civicrm/admin/extensions', 'reset=1'));
76 $url = CRM_Utils_System::url('civicrm/admin/setting/path', "reset=1&civicrmDestination=${civicrmDestination}");
77 $errors[] = array(
78 'title' => ts('Directory Unwritable'),
6a488035
TO
79 'message' => ts("Your extensions directory is not set or is not writable. Click <a href='%1'>here</a> to set the extensions directory.",
80 array(
6a488035
TO
81 1 => $url,
82 )
21dfd5f5 83 ),
6a488035
TO
84 );
85 }
86
87 if (!class_exists('ZipArchive')) {
88 $errors[] = array(
89 'title' => ts('ZIP Support Required'),
90 'message' => ts('You will not be able to install extensions at this time because your installation of PHP does not support ZIP archives. Please ask your system administrator to install the standard PHP-ZIP extension.'),
91 );
92 }
93
19ec0aa5
MWMC
94 if ($extensionInfo) {
95 $requiredExtensions = CRM_Extension_System::singleton()->getManager()->findInstallRequirements([$extensionInfo->key], $extensionInfo);
96 foreach ($requiredExtensions as $extension) {
64f6b376 97 if (CRM_Extension_System::singleton()->getManager()->getStatus($extension) !== CRM_Extension_Manager::STATUS_INSTALLED && $extension !== $extensionInfo->key) {
795b2761 98 $requiredExtensionInfo = CRM_Extension_System::singleton()->getBrowser()->getExtension($extension);
99 $requiredExtensionInfoName = empty($requiredExtensionInfo->name) ? $extension : $requiredExtensionInfo->name;
19ec0aa5
MWMC
100 $errors[] = [
101 'title' => ts('Missing Requirement: %1', [1 => $extension]),
795b2761 102 'message' => ts('You will not be able to install/upgrade %1 until you have installed the %2 extension.', [1 => $extensionInfo->name, 2 => $requiredExtensionInfoName]),
19ec0aa5
MWMC
103 ];
104 }
105 }
106 }
107
6a488035
TO
108 return $errors;
109 }
110
111 /**
fe482240 112 * Install or upgrade an extension from a remote URL.
6a488035 113 *
f41911fd
TO
114 * @param string $key
115 * The name of the extension being installed.
116 * @param string $downloadUrl
117 * URL of a .zip file.
a6c01b45
CW
118 * @return bool
119 * TRUE for success
6a488035
TO
120 * @throws CRM_Extension_Exception
121 */
122 public function download($key, $downloadUrl) {
123 $filename = $this->tmpDir . DIRECTORY_SEPARATOR . $key . '.zip';
124 $destDir = $this->containerDir . DIRECTORY_SEPARATOR . $key;
125
126 if (!$downloadUrl) {
df9f2d1a 127 throw new CRM_Extension_Exception(ts('Cannot install this extension - downloadUrl is not set!'));
6a488035
TO
128 }
129
353ffa53 130 if (!$this->fetch($downloadUrl, $filename)) {
6a488035
TO
131 return FALSE;
132 }
133
134 $extractedZipPath = $this->extractFiles($key, $filename);
353ffa53 135 if (!$extractedZipPath) {
6a488035
TO
136 return FALSE;
137 }
138
353ffa53 139 if (!$this->validateFiles($key, $extractedZipPath)) {
6a488035
TO
140 return FALSE;
141 }
142
143 $this->manager->replace($extractedZipPath);
144
145 return TRUE;
146 }
147
148 /**
149 * Download the remote zipfile.
150 *
f41911fd
TO
151 * @param string $remoteFile
152 * URL of a .zip file.
153 * @param string $localFile
154 * Path at which to store the .zip file.
ae5ffbb7 155 * @return bool
a6c01b45 156 * Whether the download was successful.
6a488035
TO
157 */
158 public function fetch($remoteFile, $localFile) {
7865cc4e 159 $client = $this->getGuzzleClient();
6cda8fd1
MW
160 $response = $client->request('GET', $remoteFile, ['sink' => $localFile, 'timeout' => \Civi::settings()->get('http_timeout')]);
161 if ($response->getStatusCode() === 200) {
162 return TRUE;
6a488035 163 }
6cda8fd1 164 return FALSE;
6a488035
TO
165 }
166
167 /**
fe482240 168 * Extract an extension from a zip file.
6a488035 169 *
f41911fd
TO
170 * @param string $key
171 * The name of the extension being installed; this usually matches the basedir in the .zip.
172 * @param string $zipFile
173 * The local path to a .zip file.
72b3a70c
CW
174 * @return string|FALSE
175 * zip file path
6a488035
TO
176 */
177 public function extractFiles($key, $zipFile) {
178 $config = CRM_Core_Config::singleton();
179
180 $zip = new ZipArchive();
181 $res = $zip->open($zipFile);
182 if ($res === TRUE) {
183 $zipSubDir = CRM_Utils_Zip::guessBasedir($zip, $key);
184 if ($zipSubDir === FALSE) {
185 CRM_Core_Session::setStatus(ts('Unable to extract the extension: bad directory structure'), '', 'error');
186 return FALSE;
187 }
188 $extractedZipPath = $this->tmpDir . DIRECTORY_SEPARATOR . $zipSubDir;
189 if (is_dir($extractedZipPath)) {
190 if (!CRM_Utils_File::cleanDir($extractedZipPath, TRUE, FALSE)) {
191 CRM_Core_Session::setStatus(ts('Unable to extract the extension: %1 cannot be cleared', array(1 => $extractedZipPath)), ts('Installation Error'), 'error');
192 return FALSE;
193 }
194 }
195 if (!$zip->extractTo($this->tmpDir)) {
196 CRM_Core_Session::setStatus(ts('Unable to extract the extension to %1.', array(1 => $this->tmpDir)), ts('Installation Error'), 'error');
197 return FALSE;
198 }
199 $zip->close();
200 }
201 else {
202 CRM_Core_Session::setStatus(ts('Unable to extract the extension.'), '', 'error');
203 return FALSE;
204 }
205
206 return $extractedZipPath;
207 }
208
209 /**
210 * Validate that $extractedZipPath contains valid for extension $key
211 *
da6b46f4
EM
212 * @param $key
213 * @param $extractedZipPath
214 *
6a488035 215 * @return bool
df9f2d1a 216 * @throws CRM_Core_Exception
6a488035 217 */
00be9182 218 public function validateFiles($key, $extractedZipPath) {
6a488035
TO
219 $filename = $extractedZipPath . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME;
220 if (!is_readable($filename)) {
221 CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error');
222 return FALSE;
223 }
224
225 try {
226 $newInfo = CRM_Extension_Info::loadFromFile($filename);
0db6c3e1
TO
227 }
228 catch (Exception $e) {
6a488035
TO
229 CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error');
230 return FALSE;
231 }
232
233 if ($newInfo->key != $key) {
df9f2d1a 234 throw new CRM_Core_Exception(ts('Cannot install - there are differences between extdir XML file and archive XML file!'));
6a488035
TO
235 }
236
237 return TRUE;
238 }
239
240}